CLI приложения #6. stdout и stderr
В прошлом посте ошибки выводились через Console.Error.WriteLine, а не через привычный Console.WriteLine. Разберёмся, зачем так.
Из любой запущенной программы выходит не одна, пусть будет, «труба» с текстом, а две.
Первая называется стандартным выводом, сокращённо stdout. В неё идёт то, ради чего программу вообще запускали: результат работы.
Вторая называется потоком ошибок, stderr. В неё идёт всё служебное: сообщения о ходе работы, предупреждения, ошибки.

схема потоков вывода
Пока вы просто смотрите в терминал, разницы никакой — обе трубы выведены на один экран, текст идёт вперемешку. Разница появляется, когда вывод программы нужно куда-то деть.
Допустим, наше приложение выгружает данные, и мы хотим сохранить результат в файл:
myapp export > data.json
Значок > — это перенаправление. Он говорит оболочке: не показывай текст на экране, а запиши его в файл. Но забирает он только первую трубу, stdout. Вторая по-прежнему идёт на экран.
И вот тут выясняется, кто как писал вывод. Если приложение печатало через Console.WriteLine вообще всё подряд, то в data.json окажется примерно такое:
text
Начинаю экспорт...
Подключение установлено
{"items":[...]}
Готово! Обработано 42 записи
Файл испорчен. Это уже не JSON, его не откроет ни одна программа, точнее открыть то откроет, но распознать json не сможет. А если бы служебные строки шли в stderr, в файл попала бы ровно одна строка с данными, а всё остальное осталось бы на экране — там, где оно и нужно человеку.
Отсюда правило: в
stdoutидёт только результат, всё остальное — вstderr.
// результат работы — его сохранят в файл или передадут дальше
Console.Out.WriteLine(json);
// служебное — человеку на экран
Console.Error.WriteLine("Обработано записей: 42");
Console.WriteLine — это то же самое, что Console.Out.WriteLine (привет Java с её system.out.printLn - похоже, не так ли)), просто короче. Когда в приложении используются оба потока, лучше писать полную форму: сразу видно, что автор выбирал осознанно, а не написал первое, что подсказал IntelliSense.
Если аналогия с трубами кажется абстрактной — вспомните обычный логгер. Там ведь тоже не один канал: LogInformation и LogError живут отдельно, и вы можете настроить, чтобы информационные сообщения шли в файл, а ошибки — ещё и в почту. Разделение по назначению.
Кстати, стандартный консольный логгер в .NET по умолчанию отправляет в stderr вообще ничего — все уровни, включая ошибки, летят в stdout. Чинится одной настройкой:
builder.Logging.AddConsole(options =>
{
options.LogToStandardErrorThreshold = LogLevel.Warning;
});
Теперь всё, что уровнем от Warning и выше, пойдёт во вторую трубу.
Важно!
Попадание вstderrне означает «случилась ошибка». Оно означает «это сообщение не является результатом работы». Прогресс-бар, до которого мы доберёмся через несколько постов, тоже пишется вstderr— иначе он изуродует любой сохранённый файл.
Полезно знать и обратную сторону: программа может выяснить, смотрит на неё человек или её вывод перенаправили.
if (Console.IsOutputRedirected)
{
// пишем в файл: голые данные, без украшений
Console.Out.WriteLine(json);
}
else
{
// человек за терминалом: можно с отступами и подсветкой
Console.Out.WriteLine(prettyJson);
}
Именно так устроен, например, git log: в терминале он красит текст и открывает его постранично, а при перенаправлении в файл отдаёт простой текст. Никакой магии, всего одна проверка.
Заведите привычку сразу: если не результат — значит,
stderr. Не придётся переделывать потом по всему проекту.
И теперь самое главное и, пожалуй, неочевидное: коды возврата, о которых мы говорили в прошлом посте, не попадают ни в один из потоков вывода!
Поэтому, правильный путь для организации и визуальной информации и отдачи корректной информации об успешности выполнении задачи - вернуть число из Main (или из SetAction), а в stderr вывести текстовое описание для пользователя:
Console.Error.WriteLine($"Файл не найден: {path}");
return ExitCode.FileNotFound; // число уходит в ОС, а не на экран