CLI приложения #5. Коды возврата

Наше приложение уже умеет команды, опции и аргументы. Но снаружи оно всегда выглядит успешным — что бы внутри ни случилось.
Проверить вывод можно так: запустите приложение с заведомо неверными аргументами, а следом:

# если PowerShell
$LASTEXITCODE

# в bash то же самое:
echo $?

Получите возврата: целое число, которое процесс отдаёт операционной системе при завершении. Ноль — всё хорошо, любое другое значение — что-то пошло не так. Больше программа сказать не может: ни текста, ни объекта, только число.
Казалось бы, мелочь. Но именно на этом числе держится вся автоматизация:

# запустить второе, только если первое отработало успешно
myapp build && myapp deploy

# CI упадёт на этом шаге, если код возврата не ноль
- run: myapp validate config.json

Если приложение всегда возвращает ноль, то в CI пайплайне всё будет хорошо, а проблемы всплывут уже в продакшене.
В C# код возврата — это то, что вернул Main. Поэтому сигнатура static int Main, а не void. В System.CommandLine число возвращается из SetAction:

internal static class ExitCode
{
    public const int Success = 0;
    public const int UnknownError = 1;
    public const int FileNotFound = 2;
    public const int InvalidFormat = 3;
}

Применяем в коде:

validateCommand.SetAction(parseResult =>
{
    var path = parseResult.GetValue(pathArgument);

    if (!File.Exists(path))
    {
        Console.Error.WriteLine($"Файл не найден: {path}");
        return ExitCode.FileNotFound;
    }

    try
    {
        JsonDocument.Parse(File.ReadAllText(path));
    }
    catch (JsonException ex)
    {
        Console.Error.WriteLine($"Некорректный JSON: {ex.Message}");
        return ExitCode.InvalidFormat;
    }

    Console.WriteLine("Файл корректен");
    return ExitCode.Success;
});

Важно!
Необработанное исключение тоже завершит процесс ненулевым кодом, но каким именно — зависит от среды выполнения, и рассчитывать на это число нельзя. Поэтому ловите исключения и возвращайте свой код.
Несколько моментов, которые стоит знать заранее:

  • Диапазон — от 0 до 255. На Unix число берётся по модулю 256, так что return 256 превратится в ноль, то есть в «успех».
  • Коды 126, 127, 128 и 130 заняты оболочкой (нет прав на запуск, команда не найдена, прерывание по Ctrl+C). Так что свои коды лучше выбирать в пределах 1–125.
  • Опишите значения кодов в справке или README. Пользователь, который пишет обвязку вокруг вашей утилиты, скажет спасибо. Ну или хотя бы не будет неприятного удивления

Кстати, обратите внимание, что в примере ошибки печатаются через Console.Error, а не привычный Console.WriteLine. Это не опечатка. Про два потока вывода поговорим в следующем посте.