CLI приложения #4. Подкоманды
До сих пор наше приложение умело ровно одну операцию. Настоящие инструменты устроены сложнее: git commit, docker run, dotnet build. Первое слово после имени программы — это не опция, а подкоманда, и у каждой свой набор параметров.
Сделаем работу с настройками, как в git:
myapp config set theme dark
myapp config get theme
myapp --verbose config set theme dark

Дерево команд
Код:
using System.CommandLine;
internal class Program
{
private static int Main(string[] args)
{
// Recursive = true — опция видна и в корне, и во всех подкомандах
var verboseOption = new Option<bool>("--verbose", "-v")
{
Description = "Подробный вывод",
Recursive = true
};
var setKeyArgument = new Argument<string>("key")
{
Description = "Имя настройки"
};
var setValueArgument = new Argument<string>("value")
{
Description = "Значение настройки"
};
var setCommand = new Command("set", "Записать настройку");
setCommand.Arguments.Add(setKeyArgument);
setCommand.Arguments.Add(setValueArgument);
setCommand.SetAction(parseResult =>
{
var key = parseResult.GetValue(setKeyArgument);
var value = parseResult.GetValue(setValueArgument);
if (parseResult.GetValue(verboseOption))
Console.WriteLine("Открываем файл настроек...");
Console.WriteLine($"Записано: {key} = {value}");
return 0;
});
var getKeyArgument = new Argument<string>("key")
{
Description = "Имя настройки"
};
var getCommand = new Command("get", "Прочитать настройку");
// Добавляем псевдоним (аналог, можно использовать "show" вместо команды "get" )
getCommand.Aliases.Add("show");
getCommand.Arguments.Add(getKeyArgument);
getCommand.SetAction(parseResult =>
{
Console.WriteLine($"Значение: {parseResult.GetValue(getKeyArgument)}");
return 0;
});
var configCommand = new Command("config", "Работа с настройками");
configCommand.Subcommands.Add(setCommand);
configCommand.Subcommands.Add(getCommand);
var rootCommand = new RootCommand("Учебное CLI приложение");
rootCommand.Options.Add(verboseOption);
rootCommand.Subcommands.Add(configCommand);
return rootCommand.Parse(args).Invoke();
}
}
Логика построения достаточно простая: соединяем подкоманды через SetAction , дерево вложенности может быть до трёх уровней.
Теперь посмотрим, что получается.
- Справка стала иерархической.
dotnet run -- --helpпокажет список команд, аdotnet run -- config --help— только то, что относится кconfig. Пользователю не нужно продираться через полотно из пятидесяти опций, чтобы найти нужные три. - У команды
configнет своегоSetAction— и это сделано намеренно. Вызовитеdotnet run -- configбез подкоманды, и приложение само покажет справку по этой ветке. Ровно так же ведёт себяgit remoteилиdotnet tool. - Опция
--verboseработает в любом месте строки: и до подкоманды, и после. За это отвечаетRecursive = true. ВнутриSetActionона читается как обычно, независимо от того, где пользователь её написал. - Алиас
showдля командыget— одна строка. Пользователь наберёт привычное ему слово, и оба варианта отработают одинаково.
Ещё важный момент, не столько про код, сколько про архитектуру и планирование: хорошей практикой будет перед написанием кода набросать план и, возможно, схему, чтобы определить, какая команда как и где будет вызываться. Дерево команд - это пользовательский интерфейс и менять его после публикации не очень дружелюбно по отношению к пользователям.
В следующих постах поговорим про коды возврата и про взаимодействие приложения с внешним миром в принципе.