Colouring user input is difficult because in half the cases, it is output by the terminal driver (with local echo) so in that case, no application running in that terminal may know when the user is going to type text and change the output colour accordingly. Only the pseudo-terminal driver (in the kernel) knows (the terminal emulator (like xterm) sends it some characters upon some keypress and the terminal driver may send back some characters for echo, but xterm can't know whether those are from the local echo or from what the application output to the slave side of the pseudo terminal).
And then, there's the other mode where the terminal driver is told not to echo, but the application this time outputs something. The application (like those using readline like gdb, bash...) may send that on its stdout or stderr which is going to be difficult to differentiate from something that it outputs for other things than echoing back the user input.
Then to differentiate an application's stdout from its stderr, there are several approaches.
Many of them involve redirecting the commands stdout and stderr to pipes and those pipes read by an application to colour it. There are two problems with that:
- Once stdout is no longer a terminal (like a pipe instead), many application tend to adapt their behavior to start buffering their output which means that output is going to be displayed in big chunks.
- Even if it's the same process that processes the two pipes, there's no guarantee that the order the text written by the application on stdout and stderr will be preserved, as the reading process can't know (if there's something to be read from both) whether to start reading from the "stdout" pipe or the "stderr" pipe.
Another approach is to modify the application so that it does colour its stdout and stdin. It is often not possible or realistic to do.
Then a trick (for dynamically linked applications) can be to hijack (using $LD_PRELOAD
as in sickill's answer) the outputting functions called by the application to output something and include code in them that sets the foreground colour based on whether they're meant to output something on stderr or stdout. However, that means hijacking every possible function from the C library and any other library that does a write(2)
syscall directly called by the application that may potentially end up writing something on stdout or stderr (printf, puts, perror...), and even then, that may modify its behavior.
Another approach could be to use PTRACE tricks as strace
or gdb
do to hook ourself every time the write(2)
system call is called and set the output colour based on whether the write(2)
is on file descriptor 1 or 2.
However, that's quite a big thing to do.
A trick which I've just been playing with is to hijack strace
itself (which does the dirty work of hooking itself before every system call) using LD_PRELOAD, to tell it to change the output colour based on whether it has detected a write(2)
on fd 1 or 2.
From looking at strace
source code, we can see that all it outputs is done via the vfprintf
function. All we need to do is to hijack that function.
The LD_PRELOAD wrapper would look like:
#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);
}
Then, we compile it with:
cc -Wall -fpic -shared -o wrap.so wrap.c -ldl
And use it as:
LD_PRELOAD=/path/to/wrap.so strace -qqf -a0 -s0 -o /dev/null \
-e write -e status=successful -P "$(tty)" \
env -u LD_PRELOAD some-cmd
You'll notice how if you replace some-cmd
with bash
, the bash prompt and what you type appears in red (stderr) while with zsh
it appears in black (because zsh dups stderr onto a new fd to display its prompt and echo).
It does appear to work surprisingly well even for applications that you'd expect not (like those that do use colours).
The colouring mode is output on strace
's stderr which is assumed to be the terminal. With -P "$(tty)"
, we're avoid doing it for writes that don't go to the terminal like when stdout/stderr have been redirected.
That solution has its limitations:
- Those inherent to
strace
: performance issues, you can't run other PTRACE commands like strace
or gdb
in it, or setuid/setgid issues
- It's colouring based on the
write
s on stdout/stderr of each individual process. So for instance, in sh -c 'echo error >&2'
, error
would be green because echo
outputs it on its stdout (which sh redirected to sh's stderr, but all strace sees is a write(1, "error\n", 6)
).
October 2021 edit. That wrapper no longer works here on a Debian unstable with strace 5.10, glibc 2.32, gcc 10.30.0, as the function to wrap now needs to be __vfprintf_chk
and the formats to look for have changed. The wrapper needs to be changed to:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>
int __vfprintf_chk(FILE *outf, int x, const char *fmt, va_list ap)
{
static int (*orig_vfprintf) (FILE*, int, 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*, int, const char *, va_list))
dlsym (RTLD_NEXT, "__vfprintf_chk");
}
if (strcmp(fmt, "%d") == 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, "= %lu") == 0) {
if (c) write(2, "\e[m", 3);
c = 0;
}
return orig_vfprintf(outf, x, fmt, ap_orig);
}
It does show this kind of approach is a bit brittle as it uses an undocumented, unstable API (not really an API).