CLI приложения #17. Вопросы пользователю

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

Console.Write("Введите порт: ");
var input = Console.ReadLine();
var port = int.Parse(input);   // а если ввели "абв"?

И дальше начинается: проверка на пустую строку, проверка на число, проверка диапазона, цикл повторного запроса. Знакомая история — ровно как с разбором аргументов во втором посте.
Spectre.Console решает это одной строкой:

var port = AnsiConsole.Ask<int>("Введите порт:");

Тип указан явно, и библиотека сама повторит вопрос, если ответ не преобразуется в число. Значение по умолчанию тоже поддерживается:

var host = AnsiConsole.Ask("Адрес сервера:", "localhost");

Для более сложных случаев есть развёрнутая форма с собственной проверкой:

var port = AnsiConsole.Prompt(
    new TextPrompt<int>("Введите порт:")
        .DefaultValue(8080)
        .Validate(p => p is > 0 and < 65536
            ? ValidationResult.Success()
            : ValidationResult.Error("[red]Порт должен быть от 1 до 65535[/]")));

Пользователь не сможет ввести ерунду: вопрос будет повторяться с пояснением, пока ответ не окажется корректным.
Пароли и токены запрашиваются отдельно — их нельзя показывать на экране:

var token = AnsiConsole.Prompt(
    new TextPrompt<string>("Введите токен доступа:")
        .PromptStyle("red")
        .Secret());

Ввод скрывается звёздочками. Метод .Secret('\0') спрячет его полностью, не показывая даже длину.
Отдельный сюжет — выбор из списка. Просить пользователя набрать название окружения руками, когда вариантов три, — так себе идея: он ошибётся в раскладке или регистре. Даём меню:

var environment = AnsiConsole.Prompt(
    new SelectionPrompt<string>()
        .Title("В какое окружение разворачиваем?")
        .AddChoices("development", "staging", "production"));

Стрелками вверх-вниз, Enter для подтверждения. Если вариантов много, добавьте .PageSize(10) — появится прокрутка, а с .EnableSearch() ещё и поиск по мере набора.
Когда выбрать нужно несколько пунктов сразу — берём множественный вариант:

var modules = AnsiConsole.Prompt(
    new MultiSelectionPrompt<string>()
        .Title("Какие модули установить?")
        .InstructionsText("[grey](пробел — выбрать, enter — подтвердить)[/]")
        .AddChoices("api", "worker", "scheduler", "dashboard"));

Вернётся список выбранных значений.

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

И не увлекайтесь. Мастер из десяти вопросов раздражает, если то же самое можно было передать одной строкой. Хороший ориентир: спрашивайте только то, что нельзя разумно угадать, и обязательно показывайте, какую команду нужно было написать, чтобы обойтись без вопросов:

AnsiConsole.MarkupLine(
    $"[grey]Подсказка: myapp deploy --environment {environment}[/]");

Пользователь запустит мастер один раз, увидит готовую команду и в следующий раз напишет её сам. Ровно так учит пользоваться собой gh от GitHub.