Ориентация главы

Консоль — первый интерфейс CLI-инструмента с пользователем; модуль console решает её три инженерные задачи: выбор потока (нормальный вывод — в stdout, диагностика — в stderr; при композиции конвейеров взаимного загрязнения нет), обнаружение терминала (xrtConsoleIsTerminal — цвет и анимация прогресса включаются только для интерактивного терминала; при перенаправлении автоматическая деградация в чистый текст) и обновление на месте (прогресс-бары/spinner перезаписывают через возврат каретки без перевода строки — журналы и прогресс делят экран, не топча друг друга). Маленький модуль, большой опыт: пропасть доверия между инструментом, сыплющим escape-последовательностями после перенаправления, и инструментом, деградирующим элегантно, огромна; а разница в коде — несколько веток проверки.

40.1引入

Три места крушения. Первое: после tool > out.txt файл полон цветовых escape-последовательностей \033[32m — автор, зашивая ANSI-цвет, не подумал, что цель вывода — не терминал. Второе: в журнале CI прогресс-бар наделал три тысячи строк — каждое обновление spinner-а переводило строку, потому что он не знал, что stdout уже забрала труба. Третье: диагностическая информация ушла в stdout — конвейер пользователя tool | grep result получил примесь warn, и разбор сценария рухнул на месте.

Общий корень трёх сцен: консоль воспринята как нечто фиксированное, а не как среда с двумя потоками, двумя формами и необходимостью адаптации в рантайме. На деле у неё два потока (stdout/stderr, семантики разные) и две формы (интерактивный терминал / перенаправленный конвейер, возможности разные) — корректный код обнаруживает и подстраивается на ходу. Модуль console стандартизует эту адаптацию: выбор потока, запрос терминала и политика вывода «обновление действует только в терминале».

40.2概念

Сначала каркас, потом детали: понятийная зона главы развёртывается в три слоя «два потока → обнаружение и деградация → дисциплина обновления», ровно на три места крушения из введения — закончив понятийную зону, вы должны уметь пересказать решение каждого случая вслух.

Семантическое разделение двух потоков

ПотокСемантикаПотребитель
stdoutпродукт программы — данные результата, нормальный выводнижестоящие конвейеры (grep/jq/файлы)
stderrзакадровый голос программы — диагностика, предупреждения, прогрессчеловек-читатель, сбор журналов

Дисциплина разделения (пятидесятилетний осадок Unix, стоит подчиняться букве): stdout — интерфейс машины — формат стабилен, разбораем, человеческого содержимого в нём никогда; stderr — интерфейс человека — диагностика вольная, цвет не возбраняется. Нарушение разделения (диагностика в stdout) — не вопрос стиля, это поломка композируемости конвейеров — кара unix-философии всегда приходит с опозданием.

Обнаружение терминала и деградация возможностей

xrtConsoleIsTerminal(流) (поток) запрашивает, не терминал ли цель — обратите внимание: запрос по потоку; stdout может уходить в трубу, пока stderr ещё на терминале — суждения независимы. Дерево решений деградации: цвет — в терминале включён, при перенаправлении выключен; анимация прогресса — в терминале обновление на месте, при перенаправлении — троттлинг печати (строка на каждые N событий или в секунду); интерактивный запрос — в терминале можно ждать ввода, при перенаправлении — немедленный отказ и выход (на другом конце трубы никого нет за клавиатурой).

Дерево решений деградации

1Цветной выводтерминал вкл / перенаправление выкл - escape-последовательности значимы только для терминала
2Анимация прогрессатерминал - обновление на месте (с лимитом скорости) / перенаправление - троттлинг по вехам (каждые 25%)
3Интерактивный запростерминал может ждать ввода / труба - немедленный отказ и выход; на том конце нет человека
4Ширина выводатерминал - перенос по обнаруженной ширине / перенаправление - без переноса (нижестоящий сам сверстает)

Обновление на месте

Две позы вывода прогресса: дописывание строками (по строке на событие — журнальный стиль; при перенаправлении единственно верная поза) и перезапись на месте (возврат каретки без перевода строки — анимационный стиль, только терминал). Дисциплины обновления на месте (анимационный стиль, только терминал — перезапись предыдущего кадра возвратом каретки): ограничение частоты обновления (10 fps глазу достаточно, больше — расточительство); в конце — финальная печать перевода строки (иначе следующий приглашение shell приклеится к хвосту прогресс-бара); когда журналы и прогресс делят экран, прогресс монополизирует последнюю строку (перед печатью журнала строка прогресса стирается — эту форму координации и демонстрирует образец variants).

40.3示例

Полная программа: разделение вывода по потокам

Из образца репозитория examples/console/output/main.c:

examples/console/output/main.c源码 ↗

















 */

#include <xrt.h>



int main(void)
{
	/* 正常日志走 stdout;诊断信息走 stderr,两者不会被混流重排。 */
	if (
		!xrtConsoleWriteLine(
			XCONSOLE_STDOUT,
			XRT_STR_LITERAL("service started")
		) ||
		!xrtConsoleWriteLine(
			XCONSOLE_STDERR,
			XRT_STR_LITERAL("example diagnostic")
		)
	) {
		return 1;
	}

	/* 程序退出前显式冲刷,保证缓冲中的诊断信息一定落地。 */
	return xrtConsoleFlush(XCONSOLE_STDERR) ? 0 : 2;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/console/output/main.c -lws2_32 -liphlpapi$ ./a.exe(stdout)service started(stderr)example diagnostic$ ./a.exe 2>/dev/null(stdout)service started(stderr 行被丢弃:stdout 保持纯净可管道)

Что только что произошло. (1) Нормальный вывод идёт в stdout, диагностика — в stderr: контрастный эксперимент 2>/dev/null проверяет разделение напрямую. (2) Композируемость конвейеров: ./a.exe | grep service поймает только строки stdout — интерфейс машины и интерфейс человека не пересекают границы. (3) Это разделение сходится с Logger глав 38/39: консольный Sink Logger-а так же соблюдает «INFO и выше в stdout, ERROR и ниже в stderr» или по конфигурации — модуль console есть его нижележащая конвенция потоков.

Полная программа: обнаружение терминала и деградация

Из examples/console/variants/main.c:

examples/console/variants/main.c源码 ↗
















 */

#include <stdio.h>
#include <xrt.h>

int main(void)
{
	bool bOk = xrtConsoleWrite(XCONSOLE_STDOUT, XRT_STR_LITERAL("write-ok"));

	printf("=1\n");
	printf("is-terminal=%d\n",
		xrtConsoleIsTerminal(XCONSOLE_STDOUT) ? 1 : 0);
	return bOk ? 0 : 1;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/console/variants/main.c -lws2_32 -liphlpapi$ ./a.exe | catwrite-ok=1is-terminal=0

Что только что произошло. (1) Интерфейс записи работает на обоих потоках (write-ok=1). (2) is-terminal=0 — при перенаправлении в трубу обнаружение возвращает ложь; на настоящем терминале та же программа выдаст 1 (поведение на настоящем терминале в этом блоке вывода не показать — меняющаяся с целью детекция и есть её смысл). (3) Один этот if — фундамент всей логики деградации: if ( xrtConsoleIsTerminal(stdout) ) { 彩色+动画 } else { 纯文本+节流 } (цвет + анимация, иначе чистый текст + троттлинг) — остальная часть образца variants демонстрирует варианты вывода (ширина, выравнивание, префиксы) на разных потоках.

Три легко упускаемых поведения терминала

Строчная против полной буферизации: stdout, подключённый к терминалу, буферизуется построчно (сброс на каждой строке), а подключённый к трубе — полностью (сброс при заполнении) — вот почему «под трубой вывод stdout долго не появляется, а stderr выходит раньше» (stderr не буферизуется никогда). Инструменты сравнения либо зовут fflush(stdout) перед критичным выводом, либо отправляют прогресс в stderr. Запрос ширины: помимо xrtConsoleIsTerminal существует запрос ширины — таблица, переносимая по ширине терминала, под трубой должна переключаться на фиксированную ширину (традиция 80 колонок); при неудаче чтения ширины (не терминал) — значение по умолчанию. Предположение о кодировке: вывод этой главы предполагает терминал UTF-8 (магистраль главы 27); транскодирование при обнаружении консоли с традиционной кодовой страницей Windows — ответственность хост-слоя; кроссплатформенным инструментам рекомендовано указать в README «рекомендуется терминал UTF-8» — это прагматичнее, чем строить в инструменте полный транскодер. Все три поведения — рифы из серии «не знаешь — однажды напорешься»: знать об их существовании важнее, чем помнить детали.

40.4契约

  • Разделение потоков: stdout = продукт (интерфейс машины), stderr = закадровый голос (интерфейс человека); диагностика никогда не входит в stdout.
  • Обнаружение терминала: цвет/анимация/интерактивность деградируют по IsTerminal; при перенаправлении — чистый текст.
  • Дисциплина обновления: обновление на месте с лимитом скорости, финальное состояние с переводом строки, координация с журналами за последнюю строку.
  • Дружественность к конвейерам: формат вывода стабилен и годится для grep/jq; прогресс идёт в stderr или троттлится.

От примеров к инженерии: три принципа CLI-опыта

Принцип первый: тишина по умолчанию — при успехе говорите мало (помимо вывода результата, stderr говорит только когда есть что сказать); -v наращивает подробность, а не «болтливость по умолчанию плюс -q для тишины». Принцип второй: ошибки действенны — каждая ошибка в stderr несёт «что делать дальше» (нет файла → укажите путь, ошибка параметра → покажите использование); в паре с цепочкой ошибок главы 4: цепочка причин разворачивается по уровням в читаемые человеком указания к действию. Принцип третий: код выхода — это интерфейс — 0 успех, ненулевые различаются по категориям ошибок (согласовано с общими категориями ошибок главы 4: ошибки параметров/IO/тайм-ауты занимают свои интервалы значений); сценарии ветвятся по коду выхода, а не угадывают разбором текста вывода. Три принципа вместе: «пользовательский опыт» CLI — это не блеск, а предсказуемость — конвейер, сценарий и человек одинаково предсказывают его поведение.

Отношение к главам 37/38

Модуль console — поставщик нижележащих конвенций для консольного Sink Logger-а (нижний слой стека — два потока операционной системы; эта глава — обёртка запросов и конвенций; Logger собирает поверх них политику): выбор stdout/stderr у Sink (частая политика INFO→stdout, ERROR→stderr) стоит на разделении потоков этой главы; цветной текстовый формат Sink-а автоматически деградирует при перенаправлении (проверка IsTerminal выполняется внутри Sink — вы настроили цветной текстовый Sink, а под трубой он сам стал чистым текстом). Поняв эту слоистость, опция «цвет» в журнальной конфигурации перестаёт быть магическим переключателем — она лишь опускает написанный вами if (хорошая форма ловушки 1) в библиотеку. И наоборот: когда вы пишете вывод CLI напрямую через модуль console, вы располагаете тем же арсеналом детекции, что и Logger, — поведение консоли у инструментов и сервисов единообразно по всей библиотеке.

Типичные ошибки

Ловушка 1: зашитые цветные escape-последовательности (катастрофа при перенаправлении):Ловушка 1: зашитые цветные escape-последовательности (катастрофа при перенаправлении)

Симптом: файл перенаправления полон \033[..m; нижестоящие сценарии разбора сломаны escape-символами.

Причина: решение о цвете зашито в оператор вывода, без ветки «цвет только в терминале».

错误写法
printf("\033[32mOK\033[0m done\n");   /* цвет безусловно - катастрофа под трубой */
正确写法
if ( xrtConsoleIsTerminal(stdout) ) {
	printf("\033[32mOK\033[0m done\n");   /* терминал: цвет */
} else {
	printf("OK done\n");                   /* труба: чистый текст */
}
Ловушка 2: прогресс-бар заливает журнал CI (деградация обновления на месте под трубой):Ловушка 2: прогресс-бар заливает журнал CI (деградация обновления на месте под трубой)

Симптом: в журнальном файле CI десятки тысяч строк прогресса; полезная информация утоплена spinner-ом.

Причина: обновление на месте (перезапись возвратом каретки) на нетерминале вырождается в обычные переводы строк — строка на каждый кадр; ветка детекции этот сценарий не покрыла.

错误写法
while ( working ) {
	printf("\rprogress: %d%%", pct);   /* на нетерминале \r не перезаписывает - строка на кадр */
	fflush(stdout);
}
正确写法
bool bTerm = xrtConsoleIsTerminal(stderr);
while ( working ) {
	if ( bTerm ) {
		fprintf(stderr, "\rprogress: %d%%", pct);   /* терминал: перезапись */
	} else if ( pct >= next_milestone ) {
		fprintf(stderr, "progress: %d%%\n", pct);   /* труба: вехи с троттлингом */
		next_milestone += 25;
	}
}
if ( bTerm ) { fprintf(stderr, "\n"); }   /* перевод строки финального состояния */

Практические задания

Базовое: тройка перенаправлений (эксперименты по разделению потоков)

Напишите программу, печатающую результат в stdout и прогресс в stderr; проверьте тремя перенаправлениями > file (stdout на диск), 2> file (stderr на диск), | grep (принимает только stdout), что получает каждый поток, — по строке вывода на эксперимент.

Продвинутое: элегантно деградирующий прогресс-бар (анимация в терминале и вехи в трубе)

Реализуйте вывод прогресса: в терминале цветной spinner + процент с обновлением на месте (лимит 10 fps); при перенаправлении — строка-веха на каждые 25%; в конце — финальная строка в терминале. Прогоните через | cat и на настоящем терминале, сравните.

Вызов: конвейерно-дружелюбный инструмент отчётов (полная посадка трёх принципов)

Реализуйте report: таблица результатов (stdout, формат стабилен, колонки выровнены), прогресс и статистика (stderr, с троттлингом по вехам), режим --quiet (stderr молчит, остаются только ошибки). Критерии приёмки: report | grep 关键字 (ключевое слово) точно находит строки результатов без остатков прогресса; report > out.txt даёт продукт без единого escape-символа и без строк прогресса, годный прямо в diff; под --quiet в stderr только строки ошибок; три кода выхода (успех/ошибка параметра/ошибка IO) каждый верен.

Итоги

ТемаКратко
Разделение потоковstdout продукт (машина) / stderr закадровый голос (человек); диагностика не входит в stdout
Обнаружение терминалаxrtConsoleIsTerminal(流); цвет, анимация, интерактивность деградируют по нему
Обновлениеперезапись на месте с лимитом 10 fps, перевод строки финального состояния, при перенаправлении — троттлинг строк-вех
Дружественность конвейерамвывод stdout годен для grep/jq; прогресс в stderr или троттлинг по вехам
С Loggerконсольный Sink соблюдает тот же набор конвенций потоков - модуль есть нижележащий фундамент
Различия буферизацииstdout построчно в терминале / полностью в трубе; stderr никогда не буферизуется - имейте в виду при сравнении
Три принципа CLIтишина по умолчанию / ошибки действенны / код выхода - интерфейс - предсказуемость важнее блеска

📖 Справочники API в этой главе:console (24 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки