В .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-репозитории.
Подкоманды могут определять как группу других подкоманд, так и непосредственно действие, выполняемое подкомандой.
Определим для начала подкоманду 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
. Например, нам ничто не мешает определить действие для подкоманды math
:
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 библиотеками:
-
ShellProgressBar. Простая библиотека для отображения прогресса в командном окне.
-
Spectre.Console. Мощная библиотека для создания красивых консольных приложений, которая имеет множество компонентов, упрощающих создание интерфейса. Кстати, обложка для статьи была сделана с помощью этой библиотеки.
-
ConsoleGUI. Позволяет создавать полноценный GUI на основе консоли. Судя по всему, автор вдохновлялся WPF, так как в ней содержатся привычные для этого фреймворка компоненты: TextBox, CheckBox, DataGrid, DockPanel и другие.
Заключение
Библиотека System.CommandLine является полезным инструментом для создания CLI-приложений. Она даёт разработчикам гибкий инструментарий для работы командами и опциями, что позволяет сократить время разработки и создать удобный и функциональный пользовательский интерфейс.