CLI приложения #11. Двойной вывод
Вернёмся к вопросу использования CLI приложений. Глобально существует два параллельно идущих сценария: когда приложением пользуется человек и когда приложение запускается скриптом или внешним приложением. Принципиальная разница только в том, в каком виде приложение будет отдавать данные. Для человека важно, чтобы результат выполнения команд и программы в целом был наглядным, понятным человеку. Для этого мы и занимаемся в последних постах цветом, таблицами и прочими украшениями.
Во втором сценарии важно отдавать данные в понятном и строго определенном формате, который сможет разобрать другая программа. Под строго определённым тут понимается один формат, заданный для конкретного вывода.
Например, если CLI приложение при запросе отдает данные в json формате, то менять этот формат на csv в процессе обновления не стоит - это поломает использование приложения там, где оно используется. Это касается и всего API приложения в целом - его стоит продумывать заранее и стараться максимально не изменять имеющиеся команды.
Но вернёмся к выводу. Поскольку у нас есть два возможных потребителя и им требуется два различных формата отдаваемых данных, значит надо предусмотреть это заранее.
Сделаем команду check, которая осматривает папку и сообщает о найденных файлах. Полный код:
using System.CommandLine;
using System.Text.Json;
using Spectre.Console;
internal record FileStatus(string Name, long Size, bool Ok);
internal class Program
{
private static int Main(string[] args)
{
var pathArgument = new Argument<string>("path")
{
Description = "Папка для проверки",
DefaultValueFactory = _ => "."
};
var formatOption = new Option<string>("--format")
{
Description = "Формат вывода: table, json или auto",
DefaultValueFactory = _ => "auto"
};
formatOption.AcceptOnlyFromAmong("table", "json", "auto");
var checkCommand = new Command("check", "Проверить файлы в папке");
checkCommand.Arguments.Add(pathArgument);
checkCommand.Options.Add(formatOption);
checkCommand.SetAction(parseResult =>
{
var path = parseResult.GetValue(pathArgument)!;
var format = parseResult.GetValue(formatOption)!;
if (!Directory.Exists(path))
{
Console.Error.WriteLine($"Папка не найдена: {path}");
return 2;
}
var files = Directory.EnumerateFiles(path)
.Select(p => new FileInfo(p))
.Select(f => new FileStatus(f.Name, f.Length, f.Length > 0))
.ToList();
var useTable = format switch
{
"table" => true,
"json" => false,
_ => AnsiConsole.Profile.Capabilities.Interactive
};
if (useTable)
{
var table = new Table();
table.AddColumn("Файл");
table.AddColumn("Размер");
table.AddColumn("Статус");
foreach (var f in files)
{
table.AddRow(
Markup.Escape(f.Name),
$"{f.Size} б",
f.Ok ? "[green]OK[/]" : "[red]пусто[/]");
}
AnsiConsole.Write(table);
}
else
{
Console.Out.WriteLine(JsonSerializer.Serialize(files));
}
Console.Error.WriteLine($"Обработано файлов: {files.Count}");
return 0;
});
var rootCommand = new RootCommand("Учебное CLI приложение");
rootCommand.Subcommands.Add(checkCommand);
return rootCommand.Parse(args).Invoke();
}
}
Здесь files — обычный список записей FileStatus, собранный из содержимого папки. Файл считается в порядке, если он не пустой. Проверка условная, но нам сейчас важно не что мы проверяем, а как показываем результат.
Ключевых мест два.
Первое — опция --format. Метод AcceptOnlyFromAmong из System.CommandLine сам отвергнет --format xml, покажет список допустимых значений и перечислит их в справке. Руками ничего проверять не нужно.
Второе — режим auto, где программа сама решает, что показать. Нужно понять, есть ли по ту сторону живой человек, и Spectre.Console отвечает на это одним свойством:
AnsiConsole.Profile.Capabilities.Interactive
Оно вернёт false, если вывод перенаправлен в файл или передан другой программе, а также в не интерактивном сеансе — например, на сервере сборки. Это тот самый случай, когда вывод должен быть в четком формате и без дополнительных украшений.
Проверяем:
# человек за терминалом — рисуется таблица
dotnet run -- check
# перенаправили в файл — внутри чистый JSON
dotnet run -- check > report.json
# а так можно потребовать таблицу принудительно
dotnet run -- check --format table > table.txt

Все три вывода: в терминал, json и текст с форматированием.
Обратите внимание на предпоследнюю строку кода: количество обработанных файлов уходит в stderr. Поэтому в report.json попадёт только JSON, а сообщение останется на экране — о таком формате разделения вывода мы говорили ранее.