CLI приложения #3. System.CommandLine: то же самое, но по-взрослому

В прошлый раз мы парсили команды, опции и аргументы вручную. Много строк и скудный функционал. Теперь разберём использование библиотеки.

Подключаем:

# можно в графической оболочке управления пакетами, но так не спортивно ))
dotnet add package System.CommandLine

Важно!

В ноябре 2025 года библиотека вышла из беты и внутри значительно изменился API, а примеры, которые можно найти в гайдах написаны по большей части на ней (до обновления, а некоторые и после). Код из старых гайдов не соберется! Так что устанавливаем версии 2.0 и выше.

Идём дальше. Основных сущностей при работе с CLI приложениями три (а можно посчитать за 4 ):

myapp process file.txt --format json: команда, аргумент и опция

Что есть что в CLI приложении#

Теперь идём в VS и пишем код:

using System.CommandLine;

internal class Program
{
    private static int Main(string[] args)
    {
        var nameOption = new Option<string>("--name", "-n")
        {
            Description = "Имя для приветствия",
            Required = true
        };

        var countOption = new Option<int>("--count", "-c")
        {
            Description = "Сколько раз поздороваться",
            DefaultValueFactory = _ => 1
        };

        var verboseOption = new Option<bool>("--verbose", "-v")
        {
            Description = "Подробный вывод"
        };

        var rootCommand = new RootCommand("Приложение здоровается с пользователем");
        rootCommand.Options.Add(nameOption);
        rootCommand.Options.Add(countOption);
        rootCommand.Options.Add(verboseOption);

        rootCommand.SetAction(parseResult =>
        {
            var name = parseResult.GetValue(nameOption);
            var count = parseResult.GetValue(countOption);
            var verbose = parseResult.GetValue(verboseOption);

            for (int i = 0; i < count; i++)
                Console.WriteLine($"Привет, {name}!");

            if (verbose)
                Console.WriteLine($"Готово, повторов: {count}");

            return 0;
        });

        return rootCommand.Parse(args).Invoke();
    }
}

И можно запускать.

  1. Как в прошлый раз:
dotnet run -- --name Иван --count 3 -v
  1. То, чего там не было:
dotnet run -- --help

Справка сгенерирована автоматически из описания опций, которые мы дали в коде.

Забегая наперёд скажу, что в этих местах, когда пишется мультиязычное приложение (если вдруг) используются строки из ресурсов. Тогда можно подставлять описания на разных языках в зависимости от локали.

Что ещё заработало само, без единой строки нашего кода:

  • --count=3 — та самая форма, которую в прошлый раз пришлось писать отдельной веткой, и только для одного параметра
  • --count абв вместо числа выдаст понятную ошибку про неверный формат, а не просто ноль
  • без --name приложение сообщит, что обязательный параметр не указан — за это отвечает Required = true
  • -- как разделитель, после которого всё считается значениями

Про последний пункт: Когда мы пишем dotnet run -- --name Иван, эти два дефиса нужны именно затем, чтобы dotnet run забрал себе то, что слева, а нашему приложению отдал то, что справа. Без них -v уедет в dotnet как уровень детализации сборки, и вы получите ошибку от неё, а не от своей программы.

Кастомные проверки значений добавляются не менее просто, чем сами опции:

countOption.Validators.Add(result =>
{
    if (result.GetValueOrDefault<int>() < 1)
        result.AddError("Количество должно быть больше нуля");
});

В прошлый раз нужно было добавить отдельный if и в конечном итоге при десятке опций получили бы нечитаемую кашу из if-ов.

Итого.

С библиотекой кода меньше, читаемость выше, а поведение стандартное для любого CLI приложения.

Сейчас наше приложение умеет выполнять всего одну операцию. Во взрослых CLI всё несколько сложнее, например git commit, docker run, dotnet build - уже используют подкоманды. Ими мы займёмся в следующий раз.