É difícil colorir a entrada do usuário porque, na metade dos casos, ela é enviada pelo driver do terminal (com eco local), portanto, nenhum aplicativo em execução nesse terminal pode saber quando o usuário vai digitar o texto e alterar o saída de cor em conformidade. Apenas o driver do pseudo-terminal (no kernel) sabe (o emulador de terminal (como o xterm) envia alguns caracteres em algumas teclas e o driver do terminal pode enviar alguns caracteres para o eco, mas o xterm não pode saber se eles são do eco local ou de qual a saída do aplicativo para o lado do escravo do pseudo-terminal).
E então, há o outro modo em que o driver do terminal é avisado para não fazer eco, mas o aplicativo desta vez exibe algo. O aplicativo (como aqueles que usam readline como gdb, bash ...) pode enviar isso em seu stdout ou stderr, o que vai ser difícil diferenciar de algo que ele gera para outras coisas do que retornar a entrada do usuário.
Em seguida, para diferenciar o stdout de um aplicativo do seu stderr, existem várias abordagens.
Muitos deles envolvem o redirecionamento dos comandos stdout e stderr para pipes e os pipes lidos por um aplicativo para colori-lo. Existem dois problemas com isso:
- Uma vez que o stdout não é mais um terminal (como um pipe), muitos aplicativos tendem a adaptar seu comportamento para iniciar o armazenamento em buffer de sua saída, o que significa que a saída será exibida em grandes blocos.
- Mesmo que seja o mesmo processo que processa os dois canais, não há garantia de que a ordem do texto escrito pelo aplicativo em stdout e stderr será preservada, pois o processo de leitura não pode saber (se houver algo a ser lido de ambos) se deve começar a ler a partir do tubo "stdout" ou do tubo "stderr".
Outra abordagem é modificar o aplicativo para que ele colore seu stdout e stdin. Muitas vezes não é possível ou realista de fazer.
Em seguida, um truque (para aplicativos vinculados dinamicamente) pode ser seqüestrar (usando $LD_PRELOAD
como na resposta de sickill ) as funções de saída chamadas pelo aplicativo para produzir algo e incluir código nelas que define a cor de primeiro plano com base em se elas devem produzir algo em stderr ou stdout. No entanto, isso significa seqüestrar todas as funções possíveis da biblioteca C e de qualquer outra biblioteca que execute write(2)
syscall diretamente chamado pelo aplicativo que possa acabar escrevendo algo em stdout ou stderr (printf, puts, perror ...), e, mesmo assim, isso pode modificar seu comportamento.
Outra abordagem poderia ser usar truques PTRACE como strace
ou gdb
fazer para ligar-se a cada vez que a chamada de sistema write(2)
é chamada e definir a cor de saída com base no fato de write(2)
estar no descritor de arquivo 1 ou 2.
No entanto, isso é muito importante.
Um truque com o qual eu tenho jogado é seqüestrar o próprio
strace
(que faz o trabalho sujo de se ligar antes de cada chamada do sistema) usando LD_PRELOAD, para dizer a ele para mudar a cor de saída com base no fato de ter detectou um
write(2)
em fd 1 ou 2.
Olhando para strace
código-fonte, podemos ver que todas as saídas são feitas através da função vfprintf
. Tudo o que precisamos fazer é roubar essa função.
O wrapper LD_PRELOAD seria parecido com:
#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);
}
Depois, compilamos com:
cc -Wall -fpic -shared -o wrap.so wrap.c -ldl
E use-o como:
LD_PRELOAD=/path/to/wrap.so strace -qfo /dev/null -e write -s 0 env -u LD_PRELOAD some-cmd
Você notará como se você substitui some-cmd
por bash
, o prompt do bash e o que você digita aparecem em vermelho (stderr) enquanto zsh
aparece em preto (porque zsh dups stderr em um novo fd para exibir seu prompt e eco).
Parece funcionar surpreendentemente bem mesmo para aplicativos que você não esperaria (como os que usam cores).
O modo de coloração é exibido no stderr de strace
, que é considerado o terminal. Se o aplicativo redirecionar sua stdout ou stderr, nossa strace sequestrada continuará gravando as sequências de escape de coloração no terminal.
Essa solução tem suas limitações:
- Aqueles inerentes a
strace
: problemas de desempenho, você não pode executar outros comandos PTRACE como strace
ou gdb
, ou problemas setuid / setgid
- Sua coloração é baseada no
write
s no stdout / stderr de cada processo individual. Assim, por exemplo, em sh -c 'echo error >&2'
, error
seria verde porque echo
gera em seu stdout (que é redirecionado para stderr de sh, mas todo strace vê um write(1, "error\n", 6)
). E em sh -c 'seq 1000000 | wc'
, seq
faz muito ou write
s para seu stdout, então o wrapper acabará produzindo muitas sequências de escape (invisíveis) para o terminal.