La colorazione dell'input dell'utente è difficile perché nella metà dei casi viene emessa dal driver del terminale (con eco locale), in tal caso, nessuna applicazione in esecuzione in quel terminale può sapere quando l'utente sta per digitare il testo e modificare il colore di output di conseguenza . Solo il driver pseudo-terminale (nel kernel) sa (l'emulatore di terminale (come xterm) invia alcuni caratteri su un tasto premuto e il driver del terminale può inviare alcuni caratteri per l'eco, ma xterm non può sapere se quelli provengono dal eco locale o da ciò che l'applicazione invia al lato slave dello pseudo terminale).
E poi, c'è l'altra modalità in cui viene detto al driver del terminale di non eco, ma l'applicazione questa volta emette qualcosa. L'applicazione (come quelli che usano readline come gdb, bash ...) può inviarlo sul suo stdout o stderr che sarà difficile distinguere da qualcosa che emette per altre cose oltre a riecheggiare l'input dell'utente.
Quindi, per differenziare lo stdout di un'applicazione dal suo stderr, ci sono diversi approcci.
Molti di essi comportano il reindirizzamento dei comandi stdout e stderr su pipe e quei pipe letti da un'applicazione per colorarlo. Ci sono due problemi con quello:
- Una volta che stdout non è più un terminale (come una pipe invece), molte applicazioni tendono ad adattare il loro comportamento per iniziare a bufferizzare il loro output, il che significa che l'output verrà visualizzato in grossi blocchi.
- Anche se è lo stesso processo che elabora le due pipe, non vi è alcuna garanzia che l'ordine del testo scritto dall'applicazione su stdout e stderr venga preservato, poiché il processo di lettura non può sapere (se c'è qualcosa da leggere da entrambi) se iniziare a leggere dalla pipe "stdout" o dalla pipe "stderr".
Un altro approccio è quello di modificare l'applicazione in modo che colora il suo stdout e lo stdin. Spesso non è possibile o realistico da fare.
Quindi un trucco (per applicazioni collegate dinamicamente) può essere quello di dirottare (usando $LD_PRELOAD
come nella risposta di sickill ) le funzioni di emissione chiamate dall'applicazione per generare qualcosa e includere codice in esse che imposta il colore di primo piano in base al fatto che siano destinate a produrre qualcosa su stderr o stdout. Tuttavia, ciò significa dirottare ogni possibile funzione dalla libreria C e da qualsiasi altra libreria che esegue una write(2)
chiamata di chiamata diretta dall'applicazione che potrebbe potenzialmente finire per scrivere qualcosa su stdout o stderr (printf, put, perror ...), e anche allora , che potrebbe modificarne il comportamento.
Un altro approccio potrebbe essere quello di utilizzare i trucchi PTRACE come strace
o gdb
fare per agganciarsi ogni volta write(2)
che viene chiamata la chiamata di sistema e impostare il colore di output in base al fatto che write(2)
sia sul descrittore di file 1 o 2.
Tuttavia, questa è una cosa abbastanza grande da fare.
Un trucco con cui ho appena giocato è dirottare strace
se stesso (che fa il lavoro sporco di agganciarsi da solo prima di ogni chiamata di sistema) usando LD_PRELOAD, per dirgli di cambiare il colore di output in base al fatto che abbia rilevato un write(2)
su fd 1 o 2.
Osservando strace
il codice sorgente, possiamo vedere che tutto ciò che produce viene fatto tramite la vfprintf
funzione. Tutto quello che dobbiamo fare è dirottare quella funzione.
Il wrapper LD_PRELOAD sarebbe simile a:
#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);
}
Quindi, lo compiliamo con:
cc -Wall -fpic -shared -o wrap.so wrap.c -ldl
E usalo come:
LD_PRELOAD=/path/to/wrap.so strace -qfo /dev/null -e write -s 0 env -u LD_PRELOAD some-cmd
Noterai come se lo sostituisci some-cmd
con bash
, il prompt di bash e ciò che scrivi appare in rosso (stderr) mentre con zsh
esso appare in nero (perché zsh duplica stderr su un nuovo fd per visualizzare il suo prompt ed eco).
Sembra funzionare sorprendentemente bene anche per le applicazioni che non ti aspetteresti (come quelle che usano i colori).
La modalità di colorazione viene emessa sullo strace
stderr che si presume sia il terminale. Se l'applicazione reindirizza il suo stdout o stderr, la nostra sequenza dirottata continuerà a scrivere le sequenze di escape della colorazione sul terminale.
Questa soluzione ha i suoi limiti:
- Quelli inerenti a
strace
: problemi di prestazioni, non è possibile eseguire altri comandi PTRACE come strace
o gdb
in esso, o problemi setuid / setgid
- Si tratta della colorazione basata sulla
write
s su stdout / stderr di ogni singolo processo. Ad esempio, in sh -c 'echo error >&2'
, error
sarebbe verde perché lo echo
emette sul suo stdout (che sh reindirizza allo shd stderr, ma tutto lo strace vede è a write(1, "error\n", 6)
). E dentro sh -c 'seq 1000000 | wc'
, seq
fa molto write
per il suo stdout, quindi il wrapper finirà con l'output di molte sequenze di escape (invisibili) sul terminale.