Ориентация главы
Консоль — первый интерфейс 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 событий или в секунду); интерактивный запрос — в терминале можно ждать ввода, при перенаправлении — немедленный отказ и выход (на другом конце трубы никого нет за клавиатурой).
Дерево решений деградации
Обновление на месте
Две позы вывода прогресса: дописывание строками (по строке на событие — журнальный стиль; при перенаправлении единственно верная поза) и перезапись на месте (возврат каретки без перевода строки — анимационный стиль, только терминал). Дисциплины обновления на месте (анимационный стиль, только терминал — перезапись предыдущего кадра возвратом каретки): ограничение частоты обновления (10 fps глазу достаточно, больше — расточительство); в конце — финальная печать перевода строки (иначе следующий приглашение shell приклеится к хвосту прогресс-бара); когда журналы и прогресс делят экран, прогресс монополизирует последнюю строку (перед печатью журнала строка прогресса стирается — эту форму координации и демонстрирует образец variants).
40.3示例
Полная программа: разделение вывода по потокам
Из образца репозитория 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; }
Что только что произошло. (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:
*/ #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; }
Что только что произошло. (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, — поведение консоли у инструментов и сервисов единообразно по всей библиотеке.
Типичные ошибки
Симптом: файл перенаправления полон \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"); /* труба: чистый текст */ }
Симптом: в журнальном файле 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки