CLI приложения #16. Спрашивать или не спрашивать

Мы научили приложение принимать аргументы и красиво выводить результат. Но есть ситуация, которую мы обходили стороной: пользователь запустил программу и не указал то, что нужно для работы.
Вариантов поведения два, и оба варианта можно встретить в реальных инструментах.
Первый — молчаливое падение с ошибкой. Так ведёт себя git push origin без указания ветки. Второй — спросить недостающее. Так работает npm init: запускаешь без параметров и отвечаешь на вопросы.
Правильного поведения не существует, но есть правильный принцип: программа должна работать без вопросов, если ей всё передали. Спрашивать можно только то, чего не хватает, и только когда есть кому отвечать.
Вторая часть важнее первой. Представьте: ваше приложение запустили в скрипте на сервере сборки, оно не нашло нужный параметр и задало вопрос. Отвечать некому, программа ждёт ввода, сборка висит до таймаута. Отладить такое тяжело — в логе просто обрывается вывод.
Поэтому перед любым вопросом проверяем, есть ли по ту сторону человек:

if (Console.IsInputRedirected)
{
    // ввод приходит не с клавиатуры — спрашивать некого
}

Свойство IsInputRedirected вернёт true, когда данные подаются из файла или из другой программы. Есть и более общая проверка от Spectre.Console, она учитывает и другие не интерактивные случаи:

AnsiConsole.Profile.Capabilities.Interactive

Собираем логику целиком:

deployCommand.SetAction(parseResult =>
{
    var environment = parseResult.GetValue(environmentOption);

    if (string.IsNullOrEmpty(environment))
    {
        if (!AnsiConsole.Profile.Capabilities.Interactive)
        {
            Console.Error.WriteLine("Не указано окружение. Используйте --environment");
            return ExitCode.InvalidArguments;
        }

        environment = AnsiConsole.Ask<string>("В какое окружение разворачиваем?");
    }

    Console.Error.WriteLine($"Разворачиваем в {environment}");
    return ExitCode.Success;
});

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

Важно!

Сообщение об ошибке должно подсказывать решение. Не «не указано окружение», а «не указано окружение, используйте --environment». Человек, который запустил вашу программу из скрипта, не полезет читать справку — он прочитает первую строку ошибки и захочет сразу починить.

Отдельного упоминания заслуживают опасные операции. Удаление, перезапись, развёртывание на проде — такое принято подтверждать:

if (!force && !AnsiConsole.Confirm("Удалить все данные?", defaultValue: false))
{
    Console.Error.WriteLine("Отменено пользователем");
    return ExitCode.Cancelled;
}

Обратите внимание на флаг force. Опция --force или --yes — обязательный спутник любого подтверждения, иначе вашу программу невозможно использовать в автоматизации. И значение по умолчанию у опасного вопроса всегда должно быть отрицательным: пользователь, который машинально жмёт Enter, не должен потерять данные.

В следующем посте разберём, как задавать вопросы по-человечески: меню выбора, проверка ответа на лету и скрытый ввод паролей.