├── .gitignore ├── README.md ├── meson.build ├── rederr.c ├── rederr.png └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rederr 2 | 3 | `rederr` is a small tool that invokes another command and propagates its stdout as-is and its stderr in ANSI red. 4 | 5 | Just prefix any command line of your choice with `rederr` and it will make it easy for to figure out what is error and what is output. 6 | 7 | Example: 8 | 9 | ![Screenshot of rederr in action](rederr.png) 10 | 11 | ## Building: 12 | 13 | First ensure you have the `meson` and `ninja` binaries installed, as well as the 14 | required C compiler (`gcc`) and related tools. 15 | 16 | Run `meson build` in the project root directory. 17 | Next change into the build directory with `cd build/` 18 | Finally run `ninja` in that directory. 19 | You should now have a `rederr` binary which you can copy to your `$PATH`. 20 | 21 | Example: 22 | 23 | ```console 24 | james@computer:~/code/src/rederr$ meson build 25 | The Meson build system 26 | Version: 0.47.2 27 | Source dir: /home/james/code/src/rederr 28 | Build dir: /home/james/code/src/rederr/build 29 | Build type: native build 30 | Project name: rederr 31 | Project version: 1 32 | Native C compiler: cc (gcc 8.2.1 "cc (GCC) 8.2.1 20181105 (Red Hat 8.2.1-5)") 33 | Build machine cpu family: x86_64 34 | Build machine cpu: x86_64 35 | Build targets in project: 1 36 | Found ninja-1.8.2 at /usr/bin/ninja 37 | james@computer:~/code/src/rederr$ cd build/ 38 | james@computer:~/code/src/rederr/build$ ninja 39 | [2/2] Linking target rederr. 40 | james@computer:~/code/src/rederr/build$ file rederr 41 | rederr: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=904987c76a525afd91777107193a564aa2a8cbc4, with debug_info, not stripped 42 | ``` 43 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-2.1+ 2 | 3 | project('rederr', 'c', 4 | version : '1', 5 | license : 'LGPLv2+', 6 | default_options: [ 7 | 'c_std=gnu99', 8 | ] 9 | ) 10 | 11 | add_project_arguments( 12 | ['-Wextra', 13 | '-Werror=undef', 14 | '-Wlogical-op', 15 | '-Wmissing-include-dirs', 16 | '-Wold-style-definition', 17 | '-Wpointer-arith', 18 | '-Winit-self', 19 | '-Wfloat-equal', 20 | '-Wsuggest-attribute=noreturn', 21 | '-Werror=missing-prototypes', 22 | '-Werror=implicit-function-declaration', 23 | '-Werror=missing-declarations', 24 | '-Werror=return-type', 25 | '-Werror=incompatible-pointer-types', 26 | '-Werror=format=2', 27 | '-Wstrict-prototypes', 28 | '-Wredundant-decls', 29 | '-Wmissing-noreturn', 30 | '-Wimplicit-fallthrough=5', 31 | '-Wshadow', 32 | '-Wendif-labels', 33 | '-Wstrict-aliasing=2', 34 | '-Wwrite-strings', 35 | '-Werror=overflow', 36 | '-Werror=shift-count-overflow', 37 | '-Werror=shift-overflow=2', 38 | '-Wdate-time', 39 | '-Wnested-externs', 40 | '-Wno-unused-parameter', 41 | '-Wno-missing-field-initializers', 42 | '-Wno-unused-result', 43 | '-Wno-format-signedness'], 44 | language : 'c') 45 | 46 | exe = executable( 47 | 'rederr', 48 | ['rederr.c'], 49 | install: true 50 | ) 51 | -------------------------------------------------------------------------------- /rederr.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: LGPL-2.1+ */ 2 | 3 | #define _GNU_SOURCE 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | /* 23 | * Invokes a process as a child, with its stdout and stderr connected to a pair of AF_UNIX/SOCK_DGRAM sockets which 24 | * both are connected to a third AF_UNIX/SOCK_DGRAM socket we listen on. Since the two stdout/stderr sockets are bound 25 | * to different AF_UNIX "auto-bind" addresses any datagrams sent over them will be read by us coming from different 26 | * sender addresses. This allows us to maintain a single, ordered stream of stdout/stderr write ops, but still know 27 | * which datagram was an stdout and which an stderr write. We use that information to output data from stderr in red, 28 | * while leaving the data from stdout in the default color. 29 | * 30 | * Or in other words, this invokes a program and colors its stderr output red. 31 | * 32 | * Caveats: 33 | * 34 | * → Since stdout/stderr of the invoked processes are sockets these process might disable automatic flushing (like 35 | * glibc stdio might). 36 | * 37 | * → For the same reason open("/proc/self/fd/1") and open("/proc/self/fd/2") is not going to work (as sockets may not 38 | * be open()ed). This means shell scripts that use 'echo foo > /dev/stderr' will not be happy (but such scripts are 39 | * slightly ugly anyway, and should rather use 'echo foo >&2'). 40 | * 41 | * → Since stdout/stderr is not a TTY there's no real interactivity. Programs that become interactive when invoked on a 42 | * tty (such as most shells) will hence remain in non-interactive mode. 43 | * 44 | */ 45 | 46 | #define ANSI_RED "\x1B[0;1;31m" 47 | #define ANSI_NORMAL "\x1B[0m" 48 | 49 | union sockaddr_union { 50 | struct sockaddr sa; 51 | struct sockaddr_un un; 52 | uint8_t buffer[sizeof(struct sockaddr_un) + 1]; /* AF_UNIX socket paths don't have to be NUL terminated */ 53 | }; 54 | 55 | static int connect_socket( 56 | const struct sockaddr *sa, socklen_t salen, 57 | union sockaddr_union *ret_bound, socklen_t *ret_bound_len) { 58 | 59 | int fd = -1, r; 60 | socklen_t k; 61 | 62 | assert(sa); 63 | assert(salen > 0); 64 | assert(ret_bound); 65 | assert(ret_bound_len); 66 | 67 | /* Allocates an AF_UNIX/SOCK_DGRAM socket and connects it the specified address, after using the auto-bind 68 | * logic to acquire a local address. */ 69 | 70 | fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0); 71 | if (fd < 0) { 72 | r = -errno; 73 | fprintf(stderr, "Failed to allocate stdout sending socket: %m\n"); 74 | goto fail; 75 | } 76 | 77 | assert(salen >= sizeof(sa_family_t)); 78 | assert(sa->sa_family == AF_UNIX); 79 | 80 | /* We reuse the socket address we are connecting to here, as for Linux' auto-bind feature we just need a 81 | * structure with AF_UNIX in the .sa_family field, and we know this one qualifies. */ 82 | if (bind(fd, sa, sizeof(sa_family_t)) < 0) { 83 | r = -errno; 84 | fprintf(stderr, "Failed to bind socket: %m\n"); 85 | goto fail; 86 | } 87 | 88 | if (connect(fd, sa, salen) < 0) { 89 | r = -errno; 90 | fprintf(stderr, "Failed to connect to our own socket: %m\n"); 91 | goto fail; 92 | } 93 | 94 | k = sizeof(union sockaddr_union); 95 | if (getsockname(fd, &ret_bound->sa, &k) < 0) { 96 | r = -errno; 97 | fprintf(stderr, "Failed to get auto-bound socket address: %m\n"); 98 | goto fail; 99 | } 100 | 101 | if (shutdown(fd, SHUT_RD) < 0) { 102 | r = -errno; 103 | fprintf(stderr, "Failed to shut down read side of socket: %m\n"); 104 | goto fail; 105 | } 106 | 107 | *ret_bound_len = k; 108 | return fd; 109 | 110 | fail: 111 | if (fd >= 0) 112 | (void) close(fd); 113 | 114 | return r; 115 | } 116 | 117 | static int allocate_sockets( 118 | int *ret_recv_fd, int *ret_send1_fd, int *ret_send2_fd, 119 | union sockaddr_union *ret_send1_sa, socklen_t *ret_send1_salen, 120 | union sockaddr_union *ret_send2_sa, socklen_t *ret_send2_salen) { 121 | 122 | int r, recv_fd = -1, send1_fd = -1, send2_fd = -1, k; 123 | bool directory_made = false, socket_bound = false; 124 | char directory[] = "/tmp/rederr.XXXXXX"; 125 | socklen_t recv_salen, send1_salen, send2_salen; 126 | union sockaddr_union recv_sa = { 127 | .un.sun_family = AF_UNIX, 128 | }, send1_sa, send2_sa; 129 | 130 | assert(ret_recv_fd); 131 | assert(ret_send1_fd); 132 | assert(ret_send2_fd); 133 | assert(ret_send1_sa); 134 | assert(ret_send1_salen); 135 | assert(ret_send2_sa); 136 | assert(ret_send2_salen); 137 | 138 | if (!mkdtemp(directory)) { 139 | r = -errno; 140 | fprintf(stderr, "Failed to create temporary directory: %m\n"); 141 | goto fail; 142 | } 143 | 144 | directory_made = true; 145 | 146 | k = snprintf(recv_sa.un.sun_path, sizeof(recv_sa.un.sun_path), "%s/sock", directory); 147 | assert(k >= 0); 148 | assert((size_t) k <= sizeof(recv_sa.un.sun_path)); 149 | recv_salen = offsetof(struct sockaddr_un, sun_path) + k + 1; 150 | 151 | recv_fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); 152 | if (recv_fd < 0) { 153 | r = -errno; 154 | fprintf(stderr, "Failed to allocate reception socket: %m\n"); 155 | goto fail; 156 | } 157 | 158 | if (bind(recv_fd, &recv_sa.sa, recv_salen) < 0) { 159 | r = -errno; 160 | fprintf(stderr, "Failed to bind socket: %m\n"); 161 | goto fail; 162 | } 163 | 164 | socket_bound = true; 165 | 166 | /* Now connect two sending socket to this. We'll use one for stdout and one for stderr of the child process we fork off */ 167 | send1_fd = connect_socket(&recv_sa.sa, recv_salen, &send1_sa, &send1_salen); 168 | if (send1_fd < 0) { 169 | r = send1_fd; 170 | goto fail; 171 | } 172 | 173 | send2_fd = connect_socket(&recv_sa.sa, recv_salen, &send2_sa, &send2_salen); 174 | if (send2_fd < 0) { 175 | r = send2_fd; 176 | goto fail; 177 | } 178 | 179 | /* Now, let's remove the socket and its temporary directory, so that we know that nobody else can connect anymore */ 180 | if (unlink(recv_sa.un.sun_path) < 0) { 181 | r = -errno; 182 | fprintf(stderr, "Failed to unlink socket: %m\n"); 183 | goto fail; 184 | } 185 | 186 | if (rmdir(directory) < 0) { 187 | r = -errno; 188 | fprintf(stderr, "Failed to remove temporary directory: %m\n"); 189 | goto fail; 190 | } 191 | 192 | *ret_recv_fd = recv_fd; 193 | *ret_send1_fd = send1_fd; 194 | *ret_send2_fd = send2_fd; 195 | 196 | memcpy(ret_send1_sa, &send1_sa, send1_salen); 197 | *ret_send1_salen = send1_salen; 198 | 199 | memcpy(ret_send2_sa, &send2_sa, send2_salen); 200 | *ret_send2_salen = send2_salen; 201 | 202 | return 0; 203 | 204 | fail: 205 | if (recv_fd >= 0) 206 | (void) close(recv_fd); 207 | if (send1_fd >= 0) 208 | (void) close(send1_fd); 209 | if (send2_fd >= 0) 210 | (void) close(send2_fd); 211 | if (socket_bound) 212 | (void) unlink(recv_sa.un.sun_path); 213 | if (directory_made) 214 | (void) rmdir(directory); 215 | 216 | return r; 217 | } 218 | 219 | static void sigchld(int sig) {} 220 | 221 | static int move_fd_up(int *fd) { 222 | int moved; 223 | 224 | assert(fd); 225 | 226 | if (*fd >= 3) 227 | return 0; 228 | 229 | moved = fcntl(*fd, F_DUPFD_CLOEXEC, 3); 230 | if (moved < 0) 231 | return -errno; 232 | 233 | (void) close(*fd); 234 | *fd = moved; 235 | 236 | return 0; 237 | } 238 | 239 | static int go(char *const *cmdline) { 240 | 241 | bool dead = false, old_ss_valid = false, old_sa_valid = false; 242 | int recv_fd = -1, send1_fd = -1, send2_fd = -1, r; 243 | union sockaddr_union send1_sa, send2_sa; 244 | socklen_t send1_salen, send2_salen; 245 | struct sigaction old_sa, new_sa = { 246 | .sa_handler = sigchld, 247 | .sa_flags = SA_NOCLDSTOP, 248 | }; 249 | size_t buffer_size = 4096; 250 | sigset_t new_ss, old_ss; 251 | void *buffer = NULL; 252 | pid_t child_pid = 0; 253 | siginfo_t si; 254 | 255 | assert(cmdline); 256 | assert(cmdline[0]); /* at least one argument before NULL */ 257 | 258 | if (sigaction(SIGCHLD, &new_sa, &old_sa) < 0) { 259 | r = -errno; 260 | fprintf(stderr, "Failed to set up SIGCHLD handler: %m"); 261 | goto finish; 262 | } 263 | 264 | old_sa_valid = true; 265 | 266 | if (sigemptyset(&new_ss) < 0 || 267 | sigaddset(&new_ss, SIGCHLD) < 0) { 268 | r = -errno; 269 | fprintf(stderr, "Failed to initialize signal mask: %m"); 270 | goto finish; 271 | } 272 | 273 | if (sigprocmask(SIG_BLOCK, &new_ss, &old_ss) < 0) { 274 | r = -errno; 275 | fprintf(stderr, "Failed to set up new signal mask: %m"); 276 | goto finish; 277 | } 278 | 279 | old_ss_valid = true; 280 | 281 | r = allocate_sockets(&recv_fd, &send1_fd, &send2_fd, &send1_sa, &send1_salen, &send2_sa, &send2_salen); 282 | if (r < 0) 283 | goto finish; 284 | 285 | child_pid = fork(); 286 | if (child_pid < 0) { 287 | r = -errno; 288 | fprintf(stderr, "Failed to fork payload process: %m\n"); 289 | goto finish; 290 | } 291 | if (child_pid == 0) { /* Child */ 292 | /* Not strictly necessary, uses O_CLOEXEC anyway */ 293 | (void) close(recv_fd); 294 | 295 | /* First move the two file descriptors out of the stdin/stdout/stderr range in case that's where they 296 | * are. (This is unlikely if we got executed with stdin/stdout/stderr properly initialized, as we 297 | * should, but let's rather be safe than sorry.)*/ 298 | r = move_fd_up(&send1_fd); 299 | if (r < 0) { 300 | errno = -r; 301 | fprintf(stderr, "Failed to move stdout file descriptor up: %m\n"); 302 | _exit(EXIT_FAILURE); 303 | } 304 | 305 | r = move_fd_up(&send2_fd); 306 | if (r < 0) { 307 | errno = -r; 308 | fprintf(stderr, "Failed to move stderr file descriptor up: %m\n"); 309 | _exit(EXIT_FAILURE); 310 | } 311 | 312 | /* Flush out everything before we replace stdout/stderr */ 313 | fflush(stdout); 314 | fflush(stderr); 315 | 316 | /* And now move them to the right place, turning off O_CLOEXEC */ 317 | if (dup2(send1_fd, STDOUT_FILENO) < 0) { 318 | fprintf(stderr, "Failed to move file descriptor to stdout: %m\n"); 319 | _exit(EXIT_FAILURE); 320 | } 321 | 322 | if (dup2(send2_fd, STDERR_FILENO) < 0) { 323 | fprintf(stderr, "Failed to move file descriptor to stderr: %m\n"); 324 | _exit(EXIT_FAILURE); 325 | } 326 | 327 | /* Not strictly necessary, uses O_CLOEXEC anyway */ 328 | (void) close(send1_fd); 329 | (void) close(send2_fd); 330 | 331 | execvp(cmdline[0], cmdline); 332 | fprintf(stderr, "Failed to execute '%s': %m\n", cmdline[0]); 333 | _exit(EXIT_FAILURE); 334 | } 335 | 336 | (void) close(send1_fd); 337 | send1_fd = -1; 338 | 339 | (void) close(send2_fd); 340 | send2_fd = -1; 341 | 342 | for (;;) { 343 | union sockaddr_union sa; 344 | struct pollfd pollfd = { 345 | .fd = recv_fd, 346 | .events = POLLIN, 347 | }; 348 | socklen_t salen; 349 | bool is_stderr; 350 | const void *p; 351 | ssize_t n; 352 | size_t l; 353 | int i; 354 | 355 | if (!dead) { 356 | /* Let's see if our child has died */ 357 | si = (siginfo_t) {}; 358 | 359 | if (waitid(P_PID, child_pid, &si, WNOHANG|WEXITED) < 0) { 360 | if (errno != EAGAIN) { 361 | r = -errno; 362 | fprintf(stderr, "Failed to waitid(): %m\n"); 363 | goto finish; 364 | } 365 | } else if (si.si_pid == child_pid) 366 | dead = true; /* Yupp, it's dead. */ 367 | } 368 | 369 | if (ppoll(&pollfd, 1, dead ? &(struct timespec) {} : NULL, &old_ss) < 0) { 370 | if (errno == EINTR) /* possibly SIGCHLD, let's query waitid() above */ 371 | continue; 372 | 373 | r = -errno; 374 | fprintf(stderr, "Failed to poll(): %m\n"); 375 | goto finish; 376 | } 377 | 378 | if (ioctl(recv_fd, SIOCINQ, &i) < 0) { 379 | r = -errno; 380 | fprintf(stderr, "Failed to read input buffer size: %m\n"); 381 | goto finish; 382 | } 383 | 384 | if ((size_t) i > buffer_size) { 385 | /* Grow the buffer if necessary */ 386 | buffer_size = i; 387 | 388 | free(buffer); 389 | buffer = NULL; 390 | } 391 | 392 | if (!buffer) { 393 | /* We allocate a buffer that can fit in the datagram plus the ANSI intro and outro if we need it */ 394 | buffer = malloc(strlen(ANSI_RED) + buffer_size + strlen(ANSI_NORMAL)); 395 | if (!buffer) { 396 | fprintf(stderr, "Out of memory: %m\n"); 397 | goto finish; 398 | } 399 | } 400 | 401 | salen = sizeof(sa); 402 | n = recvfrom(recv_fd, (uint8_t*) buffer + strlen(ANSI_RED), buffer_size, 0, &sa.sa, &salen); 403 | if (n < 0) { 404 | if (errno == EAGAIN) { 405 | if (dead) /* Nothing to read and our child is dead? If so, let's exit */ 406 | break; 407 | 408 | if (pollfd.revents & (POLLHUP|POLLERR)) /* Paranoia */ 409 | break; 410 | 411 | continue; 412 | } 413 | 414 | r = -errno; 415 | fprintf(stderr, "Failed to read from socket: %m\n"); 416 | goto finish; 417 | } 418 | 419 | /* Distuingish whether this is stderr or stdout by the sending socket address */ 420 | is_stderr = salen == send2_salen && memcmp(&sa, &send2_sa, salen) == 0; 421 | 422 | if (is_stderr) { 423 | /* This is stderr traffic, let's prefix it with the ANSI sequences and output this as a whole */ 424 | memcpy(buffer, ANSI_RED, strlen(ANSI_RED)); 425 | memcpy((uint8_t*) buffer + strlen(ANSI_RED) + n, ANSI_NORMAL, strlen(ANSI_NORMAL)); 426 | 427 | p = buffer; 428 | l = strlen(ANSI_RED) + n + strlen(ANSI_NORMAL); 429 | } else { 430 | /* This is stdout traffic, let's output this without any prefixes the way it is */ 431 | p = (uint8_t*) buffer + strlen(ANSI_RED); 432 | l = n; 433 | } 434 | 435 | while (l > 0) { 436 | n = write(is_stderr ? STDERR_FILENO : STDOUT_FILENO, p, l); 437 | if (n < 0) { 438 | r = -errno; 439 | fprintf(stderr, "Failed to write data: %m\n"); 440 | goto finish; 441 | } 442 | 443 | p = (const uint8_t*) p + n; 444 | l -= n; 445 | } 446 | } 447 | 448 | /* Propagate the childs exit status if it makes sense */ 449 | r = dead && si.si_code == CLD_EXITED ? si.si_status : 255; 450 | 451 | finish: 452 | if (recv_fd >= 0) 453 | (void) close(recv_fd); 454 | if (send1_fd >= 0) 455 | (void) close(send1_fd); 456 | if (send2_fd >= 0) 457 | (void) close(send2_fd); 458 | 459 | if (old_sa_valid) 460 | (void) sigaction(SIGCHLD, &old_sa, NULL); 461 | if (old_ss_valid) 462 | (void) sigprocmask(SIG_SETMASK, &old_ss, NULL); 463 | 464 | free(buffer); 465 | 466 | return r; 467 | } 468 | 469 | int main(int argc, char *argv[]) { 470 | int ret; 471 | 472 | if (argc < 2) { 473 | fprintf(stderr, "Not enough arguments, expected at least one.\n"); 474 | return EXIT_FAILURE; 475 | } 476 | 477 | ret = go(argv + 1); 478 | if (ret < 0) 479 | return EXIT_FAILURE; 480 | 481 | return ret; 482 | } 483 | -------------------------------------------------------------------------------- /rederr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poettering/rederr/4ef6ede025ddf25d61b6231687810b95d3b5ffa0/rederr.png -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | echo stdout >&1 4 | echo stderr >&2 5 | 6 | echo -n stdout >&1 7 | echo -n stderr >&2 8 | echo -n stdout >&1 9 | echo stderr >&2 10 | 11 | echo stdout >&1 12 | echo stderr >&2 13 | --------------------------------------------------------------------------------