В .NET уже несколько лет существует библиотека System.CommandLine, позволяющая быстро создавать CLI-приложения. Несмотря на то, что библиотека ещё в стадии beta, её активно используют и сами разработчики Microsoft, например, в утилите dotnet из пакета .NET SDK.
Преимущества этой библиотеки в том, что она позволяет сосредоточиться на разработке полезного функционала приложения и не тратить время на создание парсера команд, опций и аргументов, а также имеет широкие возможности для кастомизации.
Реализация простого CLI-приложения
Библиотека поддерживает следующие виды токенов:

Опции и аргументы определяются совместно через обобщённый тип Option<T>:
var left = new Option<double>( aliases: new[] { "-l", "--left" }, description: "Left operand") { IsRequired = true, Arity = ArgumentArity.ExactlyOne }; var right = new Option<double>( aliases: new[] { "-r", "--right" }, description: "Right operand") { IsRequired = true, Arity = ArgumentArity.ExactlyOne };
Полностью исходный код можно найти в нашем GitLab-репозитории.
Subcommand и command отличаются тем, что subcommand определяет группу команд, а command определяет непосредственно действие, выполняемое командой.
Определим для начала команду sum:
var sum = new Command("sum", "Sum of the two numbers"); sum.Add(left); sum.Add(right); sum.SetHandler((l, r) => Console.WriteLine(l + r), left, right);
Разделение на команды и группы команд очень условное, т.к. с точки зрения C# оба вида определяются через класс Command. Также нам ничто не мешает определить действие и для группы команд:
var math = new Command("math", "Mathematical operations"); math.Add(sum); math.SetHandler(() => Console.Write("Math command handler"));
Помимо примитивных типов, библиотека также поддерживает массивы, списки, FileInfo, DirectoryInfo и другие. Полный список можно узнать, заглянув в документацию. Если есть необходимость привязать опции к кастомному типу, то можно воспользоваться встроенным механизмом привязки через тип BinderBase<T>. Определим операцию вычитания, используя этот способ:
public record Subtract(double Left, double Right) { public double Calc() { var result = Left - Right; File.WriteAllText("result.txt", $"{result}"); return result; } } public class SubtractBinder : BinderBase<Subtract> { private readonly Option<double> _left; private readonly Option<double> _right; public SubtractBinder(Option<double> left, Option<double> right) => (_left, _right) = (left, right); protected override Subtract GetBoundValue(BindingContext bindingContext) => new Subtract( bindingContext.ParseResult.GetValueForOption(_left), bindingContext.ParseResult.GetValueForOption(_right)); }
Тогда определение хендлера для команды вычитания будет выглядеть следующим образом:
subtract.SetHandler( (sub) => Console.WriteLine(sub.Calc()), new SubtractBinder(left, right));
Осталось определить Root command через одноимённый класс RootCommand :
var root = new RootCommand("CLI app example"); root.Add(math); await root.InvokeAsync(args);
Теперь можно собрать приложение и проверить работу:
dotnet run -– math // вывод: Math command handler dotnet run -— math sum -l 4 -r 7 // вывод: 13 dotnet run -— math subtract -l 10 -r 3 // вывод: 7
Внедрение зависимостей
Жизненный цикл CLI-приложения выглядит следующим образом:
-
Вызывается некоторая команда.
-
Запускается процесс.
-
Происходит обработка данных, возвращается результат.
-
Процесс завершается.
Классические контейнеры зависимостей не рекомендуется использовать, поскольку зависимости одной команды могут быть совершенно не нужны для другой команды, а инициализация всех зависимостей может увеличить время запуска CLI-приложения. Вместо этого можно воспользоваться механизмом внедрения зависимостей для конкретного обработчика. Для этого снова понадобится класс BinderBase<T>. Определим новый класс ResultWriter, который будет записывать результат математической операции в файл:
public class ResultWriter { public void Write(double result) => File.WriteAllText("result.txt", $"{result}"); }
Теперь создадим класс ResultWriterBinder. Этот класс инкапсулирует экземпляр ResultWriter:
public class ResultWriterBinder : BinderBase<ResultWriter> { private readonly ResultWriter _resultWriter = new ResultWriter(); protected override ResultWriter GetBoundValue(BindingContext bindingContext) => _resultWriter; }
Теперь определим операцию умножения и внедрим туда экземпляр ResultWriter:
var multiply = new Command("multiply", "Multiply one number by another"); multiply.Add(left); multiply.Add(right); multiply.SetHandler((left, right, resultWriter) => { var result = left * right; resultWriter.Write(result); Console.WriteLine(result); }, left, right, new ResultWriterBinder());
Такой подход позволяет внедрять зависимости в обработчики команд независимо друг от друга.
Кастомизация вывода
Справка генерируется автоматически из описаний, которые использовались при инициализации опций и команд. Например, команда cli-app math sum -h отобразит следующее:
Description: Sum of the two numbers Usage: cli-app math sum [options] Options: -l, --left <left> (REQUIRED) Left operand -r, --right <right> (REQUIRED) Right operand -?, -h, --help Show help and usage information
При желании можно заменить любой раздел справки, например, Description или создать новый. Добавим новую строку с текстом «This is a new section» в начало справки:
using System.CommandLine.Builder; using System.CommandLine.Help; using System.CommandLine.Parsing; // остальной код var parser = new CommandLineBuilder(root) .UseDefaults() .UseHelp(ctx => ctx.HelpBuilder.CustomizeLayout(x => HelpBuilder.Default .GetLayout() .Prepend(d => Console.WriteLine("This is a new section")))) .Build();
Тогда вывод станет следующим:
This is a new section Description: Sum of the two numbers // остальной текст
Если требуется улучшить внешний вид вывода данных в целом, то сделать это можно, например, написав кастомный способ отображения при помощи методов и свойств класса Console, таких как SetCursorPosition, ForegroundColor, BackgroundColor и т.д. Либо воспользоваться 3rd-party библиотеками:
1. ShellProgressBar. Простая библиотека для отображения прогресса в командном окне.
2. Spectre.Console. Мощная библиотека для создания красивых консольных приложений, которая имеет множество компонентов, упрощающих создание интерфейса. Кстати, обложка для статьи была сделана с помощью этой библиотеки.
3. ConsoleGUI. Позволяет создавать полноценный GUI на основе консоли. Судя по всему, автор вдохновлялся WPF, так как в ней содержатся привычные для этого фреймворка компоненты: TextBox, CheckBox, DataGrid, DockPanel и другие.
Заключение
Библиотека System.CommandLine является полезным инструментом для создания CLI-приложений. Она даёт разработчикам гибкий инструментарий для работы командами и опциями, что позволяет сократить время разработки и создать удобный и функциональный пользовательский интерфейс.
ссылка на оригинал статьи https://habr.com/ru/articles/739646/
Добавить комментарий