Раскрашивать пользовательский ввод сложно, потому что в половине случаев он выводится драйвером терминала (с локальным эхом), поэтому в этом случае ни одно приложение, работающее на этом терминале, не может знать, когда пользователь собирается набирать текст и соответственно изменять цвет вывода. , Только псевдо-терминальный драйвер (в ядре) знает (эмулятор терминала (например, 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
- Это цвет, основанный на
writes на 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делает много или writes для своего стандартного вывода, так что обертка в конечном итоге будет выводить много (невидимых) escape-последовательностей в терминал.