CLI приложения #9. Spectre.Console

В предыдущих постах мы занимались ручной покраской вывода консоли. Это полезно - знать, как всё устроено и уметь делать это руками. Но, как это часто бывает, всё хорошее уже придумано до нас и пользоваться мы будем дальше готовой библиотекой Spectre.Console
Установка:

dotnet add package Spectre.Console

Помните пример из седьмого поста:

Console.WriteLine("\u001b[31mОшибка\u001b[0m: файл не найден");

Он же превращается в одну строку:

AnsiConsole.MarkupLine("[red]Ошибка[/]: файл не найден");

Разметка похожа на BBCode: открывающий тег со стилем, закрывающий — просто [/]. Внутри одного тега можно перечислять что угодно:

AnsiConsole.MarkupLine("[bold green]Готово[/]");
AnsiConsole.MarkupLine("[yellow on blue]Предупреждение[/]");
AnsiConsole.MarkupLine("[rgb(0,122,204)]Фирменный цвет[/]");
AnsiConsole.MarkupLine("[link=https://spectreconsole.net]Документация[/]");

Но главное здесь даже не краткость. В прошлом посте рассматривалось множество строк кода — проверки NO_COLOR, TERM, перенаправления. Библиотека делает всё это сама. Более того, она смотрит, сколько цветов терминал реально поддерживает, и подбирает ближайший доступный: попросили RGB, а терминал умеет шестнадцать цветов — получите похожий из шестнадцати. Вывод перенаправили в файл — разметка просто исчезнет, останется чистый текст.
А вот этой командой можно посмотреть, что о конкретном терминале знает библиотека:

AnsiConsole.WriteLine(AnsiConsole.Profile.Capabilities.ColorSystem.ToString());

Важно!

Никогда не подставляйте в разметку данные от пользователя напрямую. Строка вида Файл [config] не найден сломает разбор, а в худшем случае имя файла станет управляющей командой. Для чужих строк есть экранирование:

AnsiConsole.MarkupLine($"Файл [red]{Markup.Escape(fileName)}[/] не найден");

Ровно та же логика, что с параметрами SQL-запроса: свой шаблон — отдельно, чужие данные — отдельно.

Теперь про то, о чём мы говорили в шестой части. По умолчанию AnsiConsole пишет в stdout, и сообщения об ошибках туда отправлять не стоит. Заводим отдельную консоль для второго потока:

var errorConsole = AnsiConsole.Create(new AnsiConsoleSettings
{
    Out = new AnsiConsoleOutput(Console.Error)
});

errorConsole.MarkupLine("[red]Ошибка[/]: файл не найден");

Приятная мелочь напоследок — вывод исключений:

catch (Exception ex)
{
    AnsiConsole.WriteException(ex, ExceptionFormats.ShortenPaths);
}

Стек становится читаемым: типы, методы и пути подсвечены по-разному, а длинные пути сокращены. После стандартного полотна из ex.ToString() — совсем другое ощущение.

Важно!

Не перепутайте пакеты. Spectre.Console — это вывод: цвет, таблицы, прогресс-бары. А Spectre.Console.Cli — отдельная библиотека для разбора аргументов, то есть альтернатива System.CommandLine. Живёт в другом репозитории, версии у неё свои. Нам сейчас нужен первый.

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