Kolorowanie danych wejściowych użytkownika jest trudne, ponieważ w połowie przypadków jest ono wysyłane przez sterownik terminalu (z lokalnym echem), więc w takim przypadku żadna aplikacja działająca w tym terminalu może nie wiedzieć, kiedy użytkownik zamierza wpisać tekst i odpowiednio zmienić kolor wyjściowy . Tylko pseudo-terminalowy sterownik (w jądrze) wie (emulator terminala (jak xterm) wysyła mu pewne znaki po pewnym naciśnięciu klawisza, a sterownik terminala może odsyłać niektóre znaki dla echa, ale xterm nie może wiedzieć, czy są one z lokalne echo lub dane wyjściowe aplikacji na stronie slave pseudo terminala).
A potem jest inny tryb, w którym sterownikowi terminalu mówi się, żeby nie echo, ale aplikacja tym razem coś wypisuje. Aplikacja (podobnie jak te używające readline, takie jak gdb, bash ...) może wysłać to na swoim standardzie lub stderr, co będzie trudne do odróżnienia od czegoś, co wyprowadza dla innych rzeczy niż echo danych wejściowych użytkownika.
Następnie, aby odróżnić stdout aplikacji od stderr, istnieje kilka podejść.
Wiele z nich polega na przekierowaniu poleceń stdout i stderr do potoków i potoków odczytanych przez aplikację w celu pokolorowania. Są z tym dwa problemy:
- Kiedy stdout nie jest już terminalem (jak potok zamiast), wiele aplikacji dostosowuje swoje zachowanie, aby rozpocząć buforowanie danych wyjściowych, co oznacza, że dane wyjściowe będą wyświetlane w dużych porcjach.
- Nawet jeśli jest to ten sam proces, który przetwarza dwie potoki, nie ma gwarancji, że kolejność tekstu napisanego przez aplikację na stdout i stderr zostanie zachowana, ponieważ proces odczytu nie może wiedzieć (czy jest coś do odczytania z obu) czy rozpocząć czytanie z rury „stdout” czy „stderr”.
Innym podejściem jest zmodyfikowanie aplikacji tak, aby kolorowała swoje standardowe i standardowe. Często nie jest to możliwe ani realistyczne.
Następnie sztuczką (w przypadku dynamicznie połączonych aplikacji) może być przejęcie (użycie $LD_PRELOAD
jak w odpowiedzi Illilla ) funkcji wyjściowych wywoływanych przez aplikację w celu wypisania czegoś i umieszczenia w nich kodu określającego kolor pierwszego planu na podstawie tego, czy mają one coś wypisać na stderr lub stdout. Oznacza to jednak przejęcie każdej możliwej funkcji z biblioteki C i dowolnej innej biblioteki wykonującej write(2)
syscall bezpośrednio wywołanej przez aplikację, która może potencjalnie skończyć pisaniem czegoś na stdout lub stderr (printf, puts, perror ...), a nawet wtedy , co może zmienić jego zachowanie.
Innym podejściem może być użycie sztuczek PTRACE jako strace
lub gdb
zrobić, aby zaczepić się za każdym razem, gdy write(2)
wywoływane jest wywołanie systemowe, i ustawić kolor wyjściowy na podstawie tego, czy write(2)
jest on w deskryptorze pliku 1 czy 2.
Jest to jednak dość duża rzecz.
Sztuką, z którą właśnie bawiłem strace
się, jest przechwycenie samego siebie (co powoduje brudną robotę zaczepienia się przed każdym wywołaniem systemowym) za pomocą LD_PRELOAD, aby kazać mu zmienić kolor wyjściowy w zależności od tego, czy wykrył write(2)
na fd 1 czy 2)
Patrząc na strace
kod źródłowy, możemy zobaczyć, że wszystkie dane wyjściowe są wykonywane przez vfprintf
funkcję. Wszystko, co musimy zrobić, to przejąć tę funkcję.
Opakowanie LD_PRELOAD wyglądałoby następująco:
#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);
}
Następnie kompilujemy to z:
cc -Wall -fpic -shared -o wrap.so wrap.c -ldl
I użyj go jako:
LD_PRELOAD=/path/to/wrap.so strace -qfo /dev/null -e write -s 0 env -u LD_PRELOAD some-cmd
Zauważysz, jak w przypadku wymiany some-cmd
z bash
, wierszu bash i co piszesz pojawia się na czerwono (stderr), natomiast z zsh
wydaje się na czarno (bo zsh dups stderr na nowy fd, aby wyświetlić jego szybka i echo).
Wygląda na to, że działa zaskakująco dobrze nawet w aplikacjach, których nie spodziewałbyś się (takich jak te, które używają kolorów).
Tryb kolorowania jest wyprowadzany na strace
stderr, który jest uważany za terminal. Jeśli aplikacja przekieruje swoje standardowe wyjście lub standardowe polecenie, nasz przejęty ciąg będzie nadal zapisywał sekwencje specjalne koloru na terminalu.
To rozwiązanie ma swoje ograniczenia:
- Te związane z
strace
: problemami z wydajnością, nie można uruchamiać innych poleceń PTRACE, takich jak strace
lub gdb
w nim, ani problemów z setuid / setgid
- Kolorystyka oparta na
write
s na stdout / stderr każdego indywidualnego procesu. Tak na przykład, w sh -c 'echo error >&2'
, error
byłby zielony, ponieważ echo
wyjścia na IT jej stdout (które sh przekierowany do stderr SH, ale wszystko strace widzi to write(1, "error\n", 6)
). I in sh -c 'seq 1000000 | wc'
, seq
robi wiele lub więcej write
do swojego standardu , więc opakowanie ostatecznie wyśle wiele (niewidocznych) sekwencji ucieczki do terminala.