CLI приложения #8. Когда цвет не нужен

В прошлый раз мы научились красить вывод. Теперь научимся вовремя переставать.
Проблема простая. ANSI-коды — это символы в строке. Терминал их исполняет, а файл — просто сохраняет. Перенаправьте цветной вывод:

myapp status > report.txt

И внутри report.txt окажется примерно такое:

←[32mВсё в порядке←[0m

Файл засорён, автоматический разбор сломается, а человек читать это тоже не захочет.
Хорошая новость: у мира консольных приложений на этот счёт есть договорённости, и достаточно их соблюдать.

Проверка первая: нас перенаправили?

if (Console.IsOutputRedirected)
{
    // вывод уходит в файл или другой программе — цвет не нужен
}

Для потока ошибок есть отдельное свойство Console.IsErrorRedirected. Их полезно проверять по отдельности: результат вполне может уезжать в файл, пока сообщения для человека остаются на экране цветными.

Проверка вторая: переменная NO_COLOR
Есть неофициальный, но широко поддерживаемый стандарт: если в окружении задана переменная NO_COLOR с любым непустым значением, приложение должно отключить цвет. Её поддерживают десятки популярных утилит, и пользователю удобно выставить её один раз на всю систему.

Проверка третья: примитивный терминал
Переменная TERM со значением dumb означает, что перед нами терминал, который ничего не умеет. Встречается в старых средах и в некоторых системах сборки.

Проверка четвёртая: явное указание пользователя
Опция --no-color должна перебивать всё остальное. Полезна и обратная — --force-color: на серверах сборки вывод часто перенаправлен, но их веб-интерфейс цвет показывает прекрасно.
Собираем всё вместе:

internal static class ColorSupport
{
    public static bool IsEnabled { get; private set; } = true;

    public static void Configure(bool noColorFlag, bool forceColorFlag)
    {
        if (forceColorFlag)
        {
            IsEnabled = true;
            return;
        }

        if (noColorFlag)
        {
            IsEnabled = false;
            return;
        }

        var noColorVar = Environment.GetEnvironmentVariable("NO_COLOR");
        if (!string.IsNullOrEmpty(noColorVar))
        {
            IsEnabled = false;
            return;
        }

        if (Environment.GetEnvironmentVariable("TERM") == "dumb")
        {
            IsEnabled = false;
            return;
        }

        IsEnabled = !Console.IsOutputRedirected;
    }
}

Здесь важен порядок проверок: сначала то, что пользователь сам указал, потом настройки окружения, и только в самом конце наши догадки.
Осталось научить helper из прошлого поста молчать, когда цвет выключен:

public static string Paint(string text, string color)
    => ColorSupport.IsEnabled ? $"{color}{text}{Reset}" : text;

Одна строка — и во всём приложении цвет вывода под контролем ))).

Важно!
Цвет должен быть дополнением, а не носителем смысла. Если единственное отличие ошибки от успеха — красный оттенок, то в чёрно-белом выводе разницы не останется вовсе. Плюс около восьми процентов мужчин не различают красный и зелёный. Пишите «Ошибка:» словом, а цветом только подчёркивайте.

Проверить себя просто:

$env:NO_COLOR=1
dotnet run -- status

Если вывод стал чистым — вы всё сделали правильно.
Похоже мы изучили ручное управление цветом вывода. В следующем посте посмотрим на Spectre.Console, где всё это уже сделано за нас — вместе с таблицами, рамками и прогресс-барами.