Раскрашивать пользовательский ввод сложно, потому что в половине случаев он выводится драйвером терминала (с локальным эхом), поэтому в этом случае ни одно приложение, работающее на этом терминале, не может знать, когда пользователь собирается набирать текст и соответственно изменять цвет вывода. , Только псевдо-терминальный драйвер (в ядре) знает (эмулятор терминала (например, xterm) отправляет ему несколько символов при некотором нажатии, а драйвер терминала может отправлять обратно некоторые символы для эха, но xterm не может знать, получены ли они из локальный эхо или из того, что приложение выводит на ведомую сторону псевдотерминала).
И затем, есть другой режим, в котором драйвер терминала получает указание не отображать эхо, но приложение на этот раз что-то выводит. Приложение (например, использующее readline, такое как gdb, bash ...) может отправлять это на свой стандартный вывод или стандартный поток ошибок, что будет трудно отличить от чего-то, что оно выводит для других вещей, чем вывод пользовательского ввода.
Затем, чтобы отличить стандартный вывод приложения от стандартного, существует несколько подходов.
Многие из них включают в себя перенаправление команд stdout и stderr на каналы, и эти каналы читаются приложением для его раскрашивания. Есть две проблемы с этим:
- Когда stdout больше не является терминалом (например, конвейером), многие приложения стремятся адаптировать свое поведение, чтобы начать буферизовать свои выходные данные, что означает, что выходные данные будут отображаться большими кусками.
- Даже если один и тот же процесс обрабатывает два канала, нет гарантии, что порядок текста, написанный приложением на stdout и stderr, будет сохранен, поскольку процесс чтения не может знать (есть ли что-то, что можно прочитать из обоих) начать ли чтение с канала "stdout" или канала "stderr".
Другой подход состоит в том, чтобы изменить приложение так, чтобы оно окрашивало свои stdout и stdin. Это часто невозможно или реально сделать.
Тогда уловка (для динамически связанных приложений) может заключаться в том, чтобы захватить (используя, $LD_PRELOAD
как в ответе sickill ) функции вывода, вызываемые приложением для вывода чего-либо, и включать в них код, который устанавливает цвет переднего плана на основе того, предназначены ли они для вывода чего-либо на стандартный вывод или стандартный вывод. Тем не менее, это означает угон каждой возможной функции из библиотеки C и любой другой библиотеки, которая выполняет write(2)
системный вызов, вызываемый напрямую приложением, который может в конечном итоге записать что-то на stdout или stderr (printf, put, 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 -qfo /dev/null -e write -s 0 env -u LD_PRELOAD some-cmd
Вы заметите, как, если вы замените some-cmd
на bash
, приглашение bash и то, что вы zsh
набираете, отображаются красным (stderr), а с ним - черным (потому что zsh переводит stderr на новый fd, чтобы отобразить его приглашение и эхо).
Кажется, он работает на удивление хорошо даже для приложений, которые вы не ожидаете (например, те, которые используют цвета).
Режим окраски выводится на strace
's stderr, который считается терминалом. Если приложение перенаправляет свой stdout или stderr, наш угнанный страйс будет продолжать записывать escape-последовательности окраски на терминале.
Это решение имеет свои ограничения:
- Они присущи
strace
: проблемам с производительностью, вы не можете запускать другие команды PTRACE, такие как strace
или gdb
внутри него, или проблемы с setuid / setgid
- Это цвет, основанный на
write
s на stdout / stderr каждого отдельного процесса. Так, например, in sh -c 'echo error >&2'
, error
будет зеленым, потому что echo
выводит его на свой стандартный вывод (который sh перенаправляется на stderr sh, но все стрисы видят a write(1, "error\n", 6)
). И, in sh -c 'seq 1000000 | wc'
, seq
делает много или write
s для своего стандартного вывода, так что обертка в конечном итоге будет выводить много (невидимых) escape-последовательностей в терминал.