Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 16 из 25 ~18 мин +50 XP

Красивый вывод: цвета и таблицы в терминале

Цветной вывод в Go CLI — разница между утилитой, которой пользуются, и скриптом, который терпят: зелёное «готово» после сохранения, красная строка ошибки, выровненная таблица заметок. Разберём, из чего состоят ANSI-коды, зачем нужна библиотека fatih/color, почему цвет обязан отключаться в пайпах и как text/tabwriter строит таблицы в терминале.

Содержание урока · 9 разделов

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;Btruecolor — прямое указание цвета

Сырой вывод: константы и Printf

GO
const (
        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.

Стили проекта: один раз объявили, всюду используем

GO
import "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

GO
import "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()) во время работы этой команды?

Таблицы в терминале 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

GO
func 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

GO
func 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 и метод, выгружающий буфер.

w := tabwriter.(os.Stdout, 0, 4, 2, ' ', 0) fmt.Fprintln(w, "ID\tЗаголовок\tДата") for _, r := range rows { fmt.Fprintf(w, "%s\t%s\n", r[0], r[1]) } w.()

Соберите вывод таблицы по строкам

Расставьте: создание writer'а, заголовок, цикл по строкам, печать строки, закрытие цикла и Flush.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: цвет и таблицы

Термин

\x1b[...m

нажмите, чтобы перевернуть
Определение

ANSI-код SGR: escape-байт 0x1b, скобка, параметры, m. 32 — зелёный, 1 — жирный, 0 — сброс всех атрибутов. Терминал красит, файлы получают байты как есть.

1 / 6

Что дальше#

Вывод — вторая половина диалога с терминалом, первую делали в уроке про вопросы и подтверждения. Дальше по курсу цвета пригодятся сразу в двух местах: в итоговом проекте таблица задач печатается именно через 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 > файл.

Личные заметкиПусто
Хранятся только в вашем браузере (Cobra · Красивый вывод: цвета и таблицы в терминале)

Вопросы и ответы по уроку

Как отключить цветной вывод в 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) — тогда эти байты не участвуют в подсчёте ширины и печатаются как есть, а колонка остаётся ровной.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Bubble Tea25 уроков~10 чпрактика25 уроков уже доступны

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