Colorear la entrada del usuario es difícil porque en la mitad de los casos, es emitida por el controlador del terminal (con eco local), por lo que ninguna aplicación que se ejecute en ese terminal puede saber cuándo el usuario va a escribir texto y cambiar el color de salida en consecuencia . Solo el controlador de pseudo-terminal (en el núcleo) sabe (el emulador de terminal (como xterm) le envía algunos caracteres al presionar alguna tecla y el controlador de terminal puede enviar algunos caracteres para eco, pero xterm no puede saber si son del eco local o de lo que sale la aplicación al lado esclavo del pseudo terminal).
Y luego, está el otro modo en el que se le dice al controlador del terminal que no repita, pero esta vez la aplicación genera algo. La aplicación (como las que usan readline como gdb, bash ...) puede enviar eso en su stdout o stderr, lo que será difícil de diferenciar de algo que genera para otras cosas que no sean hacer eco de la entrada del usuario.
Luego, para diferenciar el stdout de una aplicación de su stderr, hay varios enfoques.
Muchos de ellos implican redirigir los comandos stdout y stderr a tuberías y esas tuberías leídas por una aplicación para colorearlo. Hay dos problemas con eso:
- Una vez que stdout ya no es un terminal (como una tubería), muchas aplicaciones tienden a adaptar su comportamiento para comenzar a almacenar en búfer su salida, lo que significa que la salida se mostrará en grandes fragmentos.
- Incluso si es el mismo proceso que procesa las dos canalizaciones, no hay garantía de que se mantenga el orden del texto escrito por la aplicación en stdout y stderr, ya que el proceso de lectura no puede saber (si hay algo que leer de ambos) si comenzar a leer desde la tubería "stdout" o la tubería "stderr".
Otro enfoque es modificar la aplicación para que coloree su stdout y stdin. A menudo no es posible ni realista hacerlo.
Entonces, un truco (para aplicaciones vinculadas dinámicamente) puede ser secuestrar (usando $LD_PRELOAD
como en la respuesta de sickill ) las funciones de salida llamadas por la aplicación para generar algo e incluir código en ellas que establece el color de primer plano en función de si están destinadas a generar algo en stderr o stdout. Sin embargo, eso significa secuestrar todas las funciones posibles de la biblioteca C y de cualquier otra biblioteca que write(2)
realice una llamada al sistema directamente llamada por la aplicación que podría terminar escribiendo algo en stdout o stderr (printf, Put, Perror ...), e incluso entonces , eso puede modificar su comportamiento.
Otro enfoque podría ser utilizar trucos PTRACE strace
o gdb
engancharnos cada vez write(2)
que se llama a la llamada del sistema y establecer el color de salida en función de si write(2)
está en el descriptor de archivo 1 o 2.
Sin embargo, eso es bastante importante.
Un truco con el que acabo de jugar es secuestrarse a strace
sí mismo (que hace el trabajo sucio de engancharse antes de cada llamada al sistema) usando LD_PRELOAD, para decirle que cambie el color de salida en función de si ha detectado un write(2)
fd 1 o 2)
Al mirar el strace
código fuente, podemos ver que todo lo que sale se realiza a través de la vfprintf
función. Todo lo que necesitamos hacer es secuestrar esa función.
El contenedor LD_PRELOAD se vería así:
#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);
}
Luego, lo compilamos con:
cc -Wall -fpic -shared -o wrap.so wrap.c -ldl
Y úsalo como:
LD_PRELOAD=/path/to/wrap.so strace -qfo /dev/null -e write -s 0 env -u LD_PRELOAD some-cmd
Notarás cómo si reemplazas some-cmd
con bash
, el indicador bash y lo que zsh
escribes aparece en rojo (stderr) mientras que aparece en negro (porque zsh dups stderr en un nuevo fd para mostrar su indicador y eco).
Parece funcionar sorprendentemente bien incluso para aplicaciones que no esperarías (como las que usan colores).
El modo de coloración se emite en strace
stderr, que se supone que es el terminal. Si la aplicación redirige su stdout o stderr, nuestra secuencia secuestrada seguirá escribiendo las secuencias de escape para colorear en el terminal.
Esa solución tiene sus limitaciones:
- Aquellos inherentes a
strace
: problemas de rendimiento, no puede ejecutar otros comandos PTRACE como strace
o gdb
en él, o problemas setuid / setgid
- Es una coloración basada en el
write
s en stdout / stderr de cada proceso individual. Entonces, por ejemplo, en sh -c 'echo error >&2'
, error
sería verde porque lo echo
genera en su stdout (que sh redirige a sh's stderr, pero todo lo que ve strace es a write(1, "error\n", 6)
). Y en sh -c 'seq 1000000 | wc'
, seq
hace mucho o write
s a la salida estándar, por lo que el contenedor va a terminar outputing un montón de (invisible) secuencias de escape a la terminal.