Могу ли я настроить свою оболочку так, чтобы вывод STDERR и STDOUT отображался в разных цветах?

Вопрос или проблема

Я хочу настроить свой терминал так, чтобы stderr отображался другим цветом, чем stdout; возможно, красным. Это облегчило бы различение двух потоков.

Существует ли способ конфигурировать это в .bashrc? Если нет, возможно ли это вообще?


Примечание: Этот вопрос был объединен с другим, в котором речь шла о stderr, stdout и эхо ввода пользователя, которые должны выводиться в 3 разных цветах. Ответы могут касаться любого из вопросов.

Проверьте мой проект stderred. Он использует LD_PRELOAD, чтобы перехватить вызовы write() библиотеки libc, окрашивая весь вывод stderr, идущий в терминал. (По умолчанию — красным.)

Это более сложная версия Показать только stderr на экране, но записать как stdout, так и stderr в файл.

Программы, работающие в терминале, используют один канал для связи с ним; у программ есть два выходных порта, stdout и stderr, но оба они подключены к одному и тому же каналу.

Вы можете подключить один из них к другому каналу, добавить цвет к этому каналу и объединить два канала, но это вызовет две проблемы:

  • Объединенный вывод может быть не в том же порядке, как если бы не было перенаправления. Это связано с тем, что добавленная обработка на одном из каналов занимает (немного) времени, поэтому цветной канал может быть задержан. Если происходит какое-либо буферизация, беспорядок будет еще хуже.
  • Терминалы используют последовательности управления цветом для определения цвета отображения, например, ␛[31m означает “переключиться на красный текст”. Это означает, что если какой-либо вывод, предназначенный для stdout, поступает именно в то время, когда выводится какой-либо вывод для stderr, текст будет неправильного цвета. (Еще хуже, если во время последовательности управления происходит переключение каналов, вы увидите мусор.)

В принципе, было бы возможно написать программу, которая слушает на двух псевдотерминалах¹, синхронно (т.е. не будет принимать ввод на одном канале, пока обрабатывает вывод на другом канале) и немедленно выводит на терминал с соответствующими инструкциями по изменению цвета. Вы потеряете возможность запускать программы, которые взаимодействуют с терминалом. Я не знаю об ни одной реализации этого метода.

Другой возможный подход заключался бы в том, чтобы заставить программу выводить соответствующие последовательности изменения цвета, перехватывая все функции libc, которые вызывают системный вызов write в библиотеке, загружаемой с помощью LD_PRELOAD. См. ответ ku1ik для существующей реализации или ответ Stéphane Chazelas для смешанного подхода, использующего strace.

На практике, если это применимо, я предлагаю перенаправить stderr в stdout и передать в цветизатор на основе шаблонов, такой как colortail или multitail, или специализированные цветизаторы, такие как colorgcc или colormake.

¹ псевдотерминалы. Трубопроводы не сработают из-за буферизации: источник может записывать в буфер, что нарушит синхронность с цветизатором.

Цветовая маркировка ввода пользователя сложна, потому что в половине случаев она выводится драйвером терминала (с локальным эхо), поэтому в этом случае ни одно приложение, работающее в этом терминале, может не знать, когда пользователь собирается ввести текст и изменить цвет вывода соответствующим образом. Только драйвер псевдотерминала (в ядре) знает (терминальный эмулятор (как xterm) отправляет ему некоторые символы при нажатии клавиш, и драйвер терминала может отправлять обратно некоторые символы для эха, но xterm не может знать, являются ли они из локального эха или из того, что приложение выводит на слейв-сторону псевдотерминала).

А потом, есть другой режим, когда драйвер терминала не должен делать эхо, но на этот раз приложение что-то выводит. Приложение (например, те, которые используют readline, как gdb, bash…) может отправить это на свой stdout или stderr, что будет сложно отличить от того, что оно выводит для других целей, кроме эха пользовательского ввода.

Затем, чтобы отличить stdout приложения от его stderr, существуют несколько подходов.

Многие из них предполагают перенаправление stdout и stderr команд в трубы, и эти трубы читаются приложением для изменения цвета. С этим связанные две проблемы:

  • Как только stdout перестает быть терминалом (например, становится трубой), многие приложения начинают адаптировать свое поведение, начиная буферизовать свой вывод, что означает, что вывод будет отображаться большими кусками.
  • Даже если это тот же процесс, который обрабатывает две трубы, нет гарантии, что порядок текста, записанного приложением в stdout и stderr, будет сохранен, так как читающий процесс не может знать (если что-то нужно прочитать из обоих), с какой “stdout” или “stderr” трубы начинать чтение.

Другой подход заключается в том, чтобы модифицировать приложение так, чтобы оно окрашивало свой stdout и stdin. Часто это невозможно или нереалистично.

Затем трюк (для динамически связанных приложений) может заключаться в том, чтобы перехватить (с помощью $LD_PRELOAD, как в ответе sickill) функции вывода, вызываемые приложением, чтобы выводить что-то и включать в них код, который устанавливает цвет переднего плана в зависимости от того, предназначены ли они для вывода на stderr или stdout. Однако это означает перехват каждой возможной функции из C-библиотеки и любой другой библиотеки, которая производит системный вызов write(2), вызываемый приложением, которое потенциально может записывать что-то в stdout или stderr (printf, puts, perror…) и даже тогда это может изменить его поведение.

Другим подходом может быть использование трюков PTRACE, как это делает strace или gdb, чтобы перехватывать каждый раз, когда вызывается системный вызов write(2), и устанавливать цвет вывода в зависимости от того, является ли write(2) на дескрипторе файла 1 или 2.

Однако это довольно большая задача.

Трюк, с которым я только что экспериментировал, заключается в том, чтобы перехватить сам strace (который делает грязную работу по перехвату перед каждым системным вызовом) с помощью LD_PRELOAD, чтобы сказать ему изменить цвет вывода в зависимости от того, определил ли он write(2) на fd 1 или 2.

Изучая исходный код strace, мы видим, что весь его вывод производится через функцию vfprintf. Все, что нам нужно сделать, это перехватить эту функцию.

Обертка LD_PRELOAD будет выглядеть так:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>

int vfprintf(FILE *outf, const char *fmt, va_list ap)
{
  static int (*orig_vfprintf) (FILE*, const char *, va_list) = 0;
  static int c = 0;
  va_list ap_orig;
  va_copy(ap_orig, ap);
  if (!orig_vfprintf) {
    orig_vfprintf = (int (*) (FILE*, const char *, va_list))
      dlsym (RTLD_NEXT, "vfprintf");
  }

  if (strcmp(fmt, "%ld, ") == 0) {
    int fd = va_arg(ap, long);
    switch (fd) {
    case 2:
      write(2, "\e[31m", 5);
      c = 1;
      break;
    case 1:
      write(2, "\e[32m", 5);
      c = 1;
      break;
    }
  } else if (strcmp(fmt, ") ") == 0) {
    if (c) write(2, "\e[m", 3);
    c = 0;
  }
  return orig_vfprintf(outf, fmt, ap_orig);
}

Затем мы компилируем это с:

cc -Wall -fpic -shared -o wrap.so wrap.c -ldl

И используем это как:

LD_PRELOAD=/path/to/wrap.so strace -qqf -a0 -s0 -o /dev/null \
  -e write -e status=successful -P "$(tty)" \
  env -u LD_PRELOAD some-cmd

Вы заметите, что если вы замените some-cmd на bash, то приглашение bash и то, что вы вводите, отображается красным (stderr), в то время как с zsh оно отображается черным (потому что zsh дублирует stderr на новый fd для отображения своего приглашения и эха).

Это кажется неожиданно хорошо работает даже для приложений, от которых вы этого не ожидали (как те, которые действительно используют цвета).

Цветовой режим выводится на stderr strace, который предполагается как терминал. С -P "$(tty)" мы избегаем этого для записей, которые не идут в терминал, например, когда stdout/stderr были перенаправлены.

Это решение имеет свои ограничения:

  • Связанные с strace: проблемы с производительностью, вы не можете запускать другие команды PTRACE, такие как strace или gdb в нем, или проблемы setuid/setgid
  • Это цветовая маркировка на основе write в stdout/stderr каждого отдельного процесса. Например, в sh -c 'echo error >&2' error будет зеленым, потому что echo выводит его на свой stdout (что sh перенаправил на stderr sh, но все, что strace видит, это write(1, "error\n", 6)).

Редактирование октября 2021 года. Эта обертка больше не работает здесь на Debian unstable с strace 5.10, glibc 2.32, gcc 10.30.0, так как функция, которую нужно обернуть, теперь должна быть __vfprintf_chk, и форматы, которые нужно искать, изменились. Обертка должна быть изменена на:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>

int __vfprintf_chk(FILE *outf, int x, const char *fmt, va_list ap)
{
  static int (*orig_vfprintf) (FILE*, int, const char *, va_list) = 0;
  static int c = 0;
  va_list ap_orig;
  va_copy(ap_orig, ap);
  if (!orig_vfprintf) {
    orig_vfprintf = (int (*) (FILE*, int, const char *, va_list))
      dlsym (RTLD_NEXT, "__vfprintf_chk");
  }

  if (strcmp(fmt, "%d") == 0) {
    int fd = va_arg(ap, long);
    switch (fd) {
    case 2:
      write(2, "\e[31m", 5);
      c = 1;
      break;
    case 1:
      write(2, "\e[32m", 5);
      c = 1;
      break;
    }
  } else if (strcmp(fmt, "= %lu") == 0) {
    if (c) write(2, "\e[m", 3);
    c = 0;
  }
  return orig_vfprintf(outf, x, fmt, ap_orig);
}

Это показывает, что такой подход несколько хрупкий, так как он использует нестабильный, необъявленный API (по сути, не совсем API).

Смотрите проект Hilite Майка Ширальди, который делает это для одной команды за раз.
Мой собственный gush делает это для целой сессии, но также имеет много других функций/особенностей, которые вам могут не понадобиться.

Вот доказательство концепции, которое я делал некоторое время назад.

Оно работает только в zsh.

# сделать стандартный вывод красным
rederr()
{
    while read -r line
    do
        setcolor $errorcolor
        echo "$line"
        setcolor normal
    done
}

errorcolor=red

errfifo=${TMPDIR:-/tmp}/errfifo.$$
mkfifo $errfifo
# чтобы заглушить строку, сообщающую нам номер задания фонового процесса
exec 2>/dev/null
rederr <$errfifo&
errpid=$!
disown %+
exec 2>$errfifo

Это также предполагает, что у вас есть функция под названием setcolor.

Упрощенная версия:

setcolor()
{
    case "$1" in
    red)
        tput setaf 1
        ;;
    normal)
        tput sgr0
        ;;
    esac
}

Ответ или решение

Конфигурирование терминала для отображения STDERR и STDOUT в разных цветах, таких как красный для STDERR, – это задача, которая может быть решена несколькими способами. Однако стоит отметить, что сами по себе стандартные инструменты оболочки, такие как .bashrc, не предоставляют встроенной функциональности для разделения вывода по цветам. Давайте рассмотрим возможные решения и альтернативы.

1. Использование LD_PRELOAD

Одним из наиболее популярных способовcolorize вывода STDERR является использование динамической функции загрузки LD_PRELOAD. С помощью этой техники можно создать библиотеку, которая перехватывает вызовы системных функций, таких как write(), и добавляет к выводу управляющие символы для изменения цвета.

Пример реализации:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>

int write(int fd, const void *buf, size_t count) {
    if (fd == 2) { // STDERR
        // Добавляем управляющую последовательность для красного цвета
        write(1, "\e[31m", 5); // \e[31m - код для красного цвета
    } else if (fd == 1) { // STDOUT
        // Добавляем управляющую последовательность для сброса цвета
        write(1, "\e[0m", 4); // \e[0m - сброс цвета
    }
    // Вызов оригинальной функции write
    int (*original_write)(int, const void *, size_t) = dlsym(RTLD_NEXT, "write");
    return original_write(fd, buf, count);
}

После компиляции этой библиотеки можно использовать LD_PRELOAD для её загрузки перед запуском команд:

gcc -shared -fPIC -o colorize.so colorize.c -ldl
LD_PRELOAD=./colorize.so ./your_application

2. Использование специализированных утилит

Если использование LD_PRELOAD не является оптимальным решением для вашей ситуации, существуют специализированные утилиты, такие как stderred, которые обеспечивают аналогичный функционал. Утилита использует LD_PRELOAD для перехвата системных вызовов и цветизует вывод STDERR по умолчанию в красный цвет. Установить stderred можно с помощью пакетного менеджера вашей системы или же собрать из исходников.

3. Перенаправление вывода через скрипты

Другой вариант заключается в использовании скриптов для перехвата и обработки вывода. Например, вы можете перенаправить STDERR и STDOUT в отдельныеFIFO-каналы и обрабатывать их параллельно:

mkfifo /tmp/stderr_pipe
exec 2>/tmp/stderr_pipe

while IFS= read -r line; do
    echo -e "\e[31m$line\e[0m"
done < /tmp/stderr_pipe &

# Запустите вашу команду
your_command_here

4. Ограничения и проблемы

  • Синхронность вывода: Из-за асинхронного характера обработки выводов, порядок появления сообщений может нарушиться, особенно если используются буферы.
  • Изменение поведения приложений: Некоторые приложения могут адаптировать свое поведение в зависимости от того, куда выводится их вывод (например, они могут отключать цвет в случае перенаправления).
  • Проблемы с взаимодействием: Если приложения взаимодействуют с терминалом, воздействие на цвет может быть затруднено, и некоторые данные могут отображаться неправильно.

Заключение

Конфигурирование терминала для разделения вывода STDERR и STDOUT с помощью цветового кодирования возможно, хотя и сопряжено с рядом сложностей. Использование метода LD_PRELOAD является наиболее гибким и мощным решением, тогда как другие подходы могут требовать дополнительной настройки или ограничивать функциональность вашего терминала. Таким образом, выбор подхода зависит от ваших технических навыков и потребностей в определенных задачах.

Оцените материал
Добавить комментарий

Капча загружается...