notes list сейчас печатает заметки в столбик — по одному полю на строку, и при тридцати записях человек листает терминал, как страницу из налоговой декларации. kubectl, gh, docker на том же экране выглядят иначе: заголовки выделены, статусы подкрашены, колонки выровнены так, что глаз не спотыкается. Ничего магического в этом нет — терминал умеет красить текст с восьмидесятых годов, а выравнивание решается стандартной библиотекой без единой зависимости сверх одной маленькой библиотеки для цвета. В этом уроке соберём всё, что нужно CLI на Cobra: сырые ANSI-коды, чтобы понимать механику, fatih/color, чтобы не считать байты руками, правило отключения цвета в пайпах и таблицы через text/tabwriter. Всё это поедет в итоговый проект курса — там же появится флаг --color с тремя режимами.
Цветной вывод в Go CLI: ANSI escape-коды#
Терминал рисует цвет по escape-последовательностям: это обычные байты в потоке вывода, начинающиеся с escape-символа 0x1b, за которым идёт открывающая скобка и параметры. Такая конструкция называется CSI-последовательностью, а команды, меняющие цвет и стиль, — кодами SGR. Печать \x1b[32m включает зелёный цвет текста, \x1b[1m — жирный, а \x1b[0m сбрасывает всё к обычному виду. Терминал эти байты перехватывает и красит; файл, лог-сборщик или соседняя программа по пайпу — не перехватывает, для них это мусорные символы в тексте. Из этого следует главное правило цветного вывода: красить можно только когда потребитель — живой терминал, и всегда заканчивать раскрашенный фрагмент сбросом, иначе цвет «протечёт» на весь остаток экрана. Сброс забывают чаще всего: одна красная ошибка без \x1b[0m в конце — и приглашение шелла до конца сессии красное.
Коды SGR, которые реально нужны CLI#
| Код | Что делает |
|---|---|
| 0 | сброс всех атрибутов к обычному тексту |
| 1 | жирный шрифт |
| 4 | подчёркивание |
| 30-37 | цвет текста: чёрный, красный, зелёный, жёлтый, синий, пурпурный, циан, белый |
| 90-97 | яркие варианты тех же восьми цветов |
| 38;5;N | один из 256 цветов палитры |
| 38;2;R;G;B | truecolor — прямое указание цвета |
Сырой вывод: константы и Printf
GOconst (
reset = "\x1b[0m"
green = "\x1b[32m"
red = "\x1b[31m"
)
func printSaved(n int) {
fmt.Printf("%sСохранено заметок: %d%s\n", green, n, reset)
}
func printLoadErr(err error) {
fmt.Fprintf(os.Stderr, "%sошибка: %v%s\n", red, err, reset)
}Код цвета заключён между escape-байтом и буквой m; имена констант не должны совпадать с функциями пакета color, поэтому здесь сырые константы названы в нижнем регистре и живут отдельно. Ошибка уходит в stderr — так же, как в уроке про ошибки и коды выхода. Для реального проекта такой ручной вывод не нужен: дальше по тексту библиотека закроет и цвета, и отключение.
fatih/color: цвет без ручных байтов#
Библиотека github.com/fatih/color — стандарт де-факто для цветного вывода в Go CLI, ею пользуется половина известных утилит. Тип color.Color хранит набор атрибутов: color.New(color.FgGreen, color.Bold) — зелёный жирный, а методы Sprint, Sprintf и PrintFunc возвращают раскрашенную строку с уже правильным сбросом в конце. Стили объявляются один раз в пакете как переменные — okStyle, errStyle, warnStyle — и используются во всех командах, так что «готово» везде одинаково зелёное, а не пятью оттенками. Самое ценное у библиотеки внутри: глобальный флаг NoColor. При старте color сам проверяет, смотрит ли stdout в терминал, и уважает переменную NO_COLOR — если вывели в файл или пайп, Sprint вернёт текст без единого лишнего байта. То есть правило отключения цвета уже встроено, и это основная причина взять fatih/color вместо ручных \x1b[32m.
Стили проекта: один раз объявили, всюду используем
GOimport "github.com/fatih/color"
var (
okStyle = color.New(color.FgGreen, color.Bold)
errStyle = color.New(color.FgRed)
headStyle = color.New(color.FgCyan)
)
func printSaved(n int) {
fmt.Println(okStyle.Sprintf("Сохранено заметок: %d", n))
}
func printErr(err error) {
fmt.Fprintln(os.Stderr, errStyle.Sprint("ошибка:", err))
}Sprintf у color.Color работает как fmt.Sprintf, но оборачивает результат в коды цвета и сброс; Sprint добавляет пробелы между операндами, как fmt.Sprint. Стили — это значения-значения, потокобезопасные для чтения: создавать их в каждой команде заново не нужно. Ошибки по-прежнему печатаются в stderr через Fprintln — библиотека красит строку, а выбор потока остаётся за вами.
Цвет и пайпы: isatty и NO_COLOR#
Пайп — главный потребитель вывода CLI: notes list | grep срочно, notes list > backup.txt, вызов из скрипта. Потребителю по пайпу коды цвета не нужны и вредны, поэтому вывод решает: терминал — красим, не терминал — нет. Проверку делает isatty.IsTerminal(os.Stdout.Fd()) из модуля github.com/mattn/go-isatty: он спрашивает у дескриптора stdout, тот ли это тип файла. fatih/color делает эту проверку сам при инициализации пакета, вручную она нужна, когда вы красите что-то вне color — например, склеиваете цвет с таблицей. Второй выключатель — переменная окружения NO_COLOR: соглашение с сайта no-color.org гласит, что любая непустая NO_COLOR в окружении запрещает цвет, и пользователи ставят её глобально, когда хотят цветной вывод go cli отключить раз и навсегда. fatih/color уважает NO_COLOR из коробки; в ручных проверках учитывайте её явно. Для тонкого контроля добавляют флаг --color со значениями auto, always, never: auto — по isatty, always — красить даже в пайп (для less -R), never — выключить насильно.
Ручная проверка: NO_COLOR плюс isatty
GOimport "github.com/mattn/go-isatty"
func colorEnabled() bool {
if v, ok := os.LookupEnv("NO_COLOR"); ok && v != "" {
return false
}
return isatty.IsTerminal(os.Stdout.Fd()) ||
isatty.IsCygwinTerminal(os.Stdout.Fd())
}LookupEnv отличает пустую NO_COLOR от отсутствующей: по соглашению пустая не считается запретом, а любая непустая — считается. IsCygwinTerminal добавлен для mintty в Windows — там stdout выглядит пайпом, хотя это терминал. В fatih/color эта логика уже есть; функция нужна для собственного вывода вне библиотеки, например для цветных ячеек таблицы ниже.
Что окажется в файле
Прочитайте код и выберите, что он выведет:
color.New(color.FgGreen).Print("готово")
// команду запустили так: notes list > list.txtПроверка: вывод по пайпу
notes list | head -5: что вернёт isatty.IsTerminal(os.Stdout.Fd()) во время работы этой команды?
Утилита печатала красные ошибки с ручными \x1b[31m и писала вывод в лог; ночью скрипт собирал статистику через awk по колонкам — и колонки поехали: для awk байты цвета — часть первого поля. Хуже, grep по слову ERROR находил строку, но сравнение полей в символьных логах падало. Диагноз ставится через cat -v файл.log: коды видны как ^[[31m. Правило: цвет — только для живого терминала; в лог-файлы и пайпы уходит чистый текст. Если пейджер нужен цветной — less -R, а не always для всего подряд. Проверить всё это можно только локально: песочница терминальный цвет не показывает, поэтому в уроках курса код запускается руками в своём терминале.
Таблицы в терминале Go: text/tabwriter#
За таблицы в терминале у большинства утилит отвечает text/tabwriter из стандартной библиотеки. Идея простая: вы печатаете строки, разделяя колонки символом \t, а tabwriter собирает колонки в буфер, считает ширину каждой по самой длинной ячейке и дополняет остальные пробелами до ровного столбца. Конструктор NewWriter(output, minwidth, tabwidth, padding, padchar, flags) принимает пять чисел и флаг; рабочая связка для CLI — minwidth 0, padding 2, пробел как заполнитель: ячейка получит ширину максимум плюс два пробела. Важная механика: tabwriter не знает, где таблица кончилась, и пишет в output кусками — строка с меньшим числом ячеек или вызов Flush завершают блок, и тогда буфер выгружается. Отсюда привычка ставить defer w.Flush() сразу после создания: забытый Flush — классическая ошибка, при которой терминал молчит, а программа завершилась успешно. Для таблиц, где ширина важнее простоты, есть флаг AlignRight, но в CLI чаще хватает выравнивания влево по умолчанию.
Таблица заметок через tabwriter
GOfunc printNotesTable(cmd *cobra.Command, notes []Note) {
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 4, 2, ' ', 0)
defer w.Flush()
fmt.Fprintln(w, " ID\tЗаголовок\tДата")
for _, n := range notes {
fmt.Fprintf(w, " %d\t%s\t%s\n", n.ID, n.Title, n.Date)
}
}Writer создан поверх cmd.OutOrStdout() — вывод уважает перенаправление, как и весь вывод команды. Заведённый пробел в начале каждой строки и в заголовке даёт отступ от края; последний \t после третьей колонки в заголовке дополнил и её до единой ширины. Flush стоит в defer: он сработает при любом выходе из функции и выгрузит буфер в stdout. Параметры, которые чаще всего путают: minwidth — минимальная ширина ячейки, tabwidth при печати пробелами не важен и обычно ноль, а padding — сколько пробелов добавляется к ширине самой длинной ячейки колонки. Связка 0, 4, 2 в примерах выше читается как «без минимума, отступы не трогаем, два пробела между колонками».
Проверка: забытый Flush
printNotesTable выполнилась, но w.Flush() вызвать забыли. Что увидит пользователь?
Цвет внутри таблицы: трюк с tabwriter.Escape
GOfunc printNotesTable(cmd *cobra.Command, notes []Note) {
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 4, 2, ' ', 0)
esc := string(tabwriter.Escape)
head := color.New(color.FgCyan, color.Bold).SprintFunc()
fmt.Fprintf(w, "%s%s%s\t%s%s%s\n", esc, head("ID"), esc,
esc, head("Заголовок"), esc)
for _, n := range notes {
fmt.Fprintf(w, " %d\t%s\t%s\n", n.ID, n.Title, n.Date)
}
w.Flush()
}Tabwriter считает видимую ширину по байтам, а коды цвета ширину увеличивают — заголовки разъехались бы. Обход из документации пакета: текст между двумя байтами 0xff — константа tabwriter.Escape — не участвует в подсчёте ширины и печатается как есть. SprintFunc здесь уместнее New, потому что заголовок красится многократно одной функцией-хелпером.
Соберите вывод таблицы
Впишите конструктор tabwriter и метод, выгружающий буфер.
Соберите вывод таблицы по строкам
Расставьте: создание writer'а, заголовок, цикл по строкам, печать строки, закрытие цикла и Flush.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: цвет и таблицы
Что дальше#
Вывод — вторая половина диалога с терминалом, первую делали в уроке про вопросы и подтверждения. Дальше по курсу цвета пригодятся сразу в двух местах: в итоговом проекте таблица задач печатается именно через tabwriter с цветным заголовком и флагом --color, а разбор структуры большого проекта покажет, где живут стили цвета в реальном репозитории — в отдельном файле с остальными помощниками вывода. Обзор курса и соседние темы — на странице Cobra. Домашнее задание: добавьте таблицу с tabwriter к самой многословной команде своей утилиты, заведите стили цвета одним блоком переменных и проверьте три сценария — живой терминал, пайп в head и NO_COLOR=1. Последний проверьте дважды: с пустой NO_COLOR и с NO_COLOR=1 — по соглашению пустая переменная цвет не запрещает.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Цвет — это байты: коды SGR включают атрибуты, \x1b[0m сбрасывает; терминал красит, файлы и пайпы получают мусор.
- fatih/color прячет байты за Sprint и Sprintf и сам отключает цвет, когда stdout не терминал.
- NO_COLOR — стандарт отключения цвета; жирная связка — флаг --color с auto, always и never.
- isatty.IsTerminal(os.Stdout.Fd()) отвечает на вопрос «красить ли» для вывода вне color.
- text/tabwriter выравнивает колонки по \t: NewWriter с minwidth 0 и padding 2 — рабочая связка для CLI.
- Flush обязателен: без него хвост таблицы остаётся в буфере writer'а.
- Цветной текст в ячейке оборачивайте в байты tabwriter.Escape, иначе ширина считается по кодам.
Домашнее задание
Добавьте к самой длинной команде своей утилиты таблицу через text/tabwriter: заголовок цветом из fatih/color, строки с отступом, Flush в defer. Затем завяжите флаг --color со значениями auto, always и never и поддержку NO_COLOR. Проверьте три сценария локально: живой терминал, пайп в head и NO_COLOR=1 notes list > файл.
Личные заметкиПусто
Вопросы и ответы по уроку
Как отключить цветной вывод в Go CLI?
Три механизма: переменная окружения NO_COLOR с непустым значением — пользовательский выключатель, флаг --color=never — разовый, и автоматическое отключение по isatty, когда stdout — пайп или файл. fatih/color делает третий пункт сам; ручной код читает NO_COLOR через os.LookupEnv и отличает пустое значение от отсутствия.
Почему цвет ломает grep и awk?
Коды цвета — невидимые для человека байты в строке, но не для утилит: они приклеиваются к первому и последнему слову строки, и сравнение полей или длин перестаёт совпадать. Отладить видны ли коды, помогает cat -v. Лечится отключением цвета при выводе не в терминал — правилом isatty из этого урока.
Как выровнять цветной текст внутри таблицы tabwriter?
Tabwriter считает ширину по байтам, поэтому ANSI-коды раздувают ячейку. Трюк из документации: оберните цветной фрагмент между двумя байтами 0xff — string(tabwriter.Escape) — тогда эти байты не участвуют в подсчёте ширины и печатаются как есть, а колонка остаётся ровной.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
- Автодополнение: bash, zsh, fish, powershell
- Кастомные подсказки: ValidArgsFunction
- Генерация документации: man и markdown
- Тестирование команд CLI
- Интерактивный ввод: вопросы и подтверждения
- Ошибки и коды выхода: SilenceUsage
- Хуки PreRun и PostRun: цепочки выполнения
- Справка: кастомизация help и usage
- Все курсы по Go
Следующая ступень учебного пути
Bubble Tea: TUI-приложения в терминале
Интерактивный курс Bubble Tea — TUI-фреймворка на Go: Elm-архитектура, bubbles, Lip Gloss, мышь и клавиатура. 25 уроков с примерами и итоговым приложением.
- Понимать Elm-архитектуру: Model, Update, View, Init
- Обрабатывать клавиатуру и мышь в терминале
- Писать асинхронные операции через tea.Cmd, Batch и Sequence
- Использовать bubbles: list, textinput, textarea, table, viewport, progress, spinner