From a69ad3c769dd2fdb70c61835f21c4ae5040864c9 Mon Sep 17 00:00:00 2001 From: Fredrik Foss-Indrehus Date: Mon, 30 Mar 2026 20:04:50 +0200 Subject: [PATCH] Strip ANSI escape codes when not writing to a terminal ANSI color escape codes are stripped from job output when standard output is redirected to a pipe or file, unless CLICOLOR_FORCE is set to a value other than "0", or TERM is set to "dumb". This matches ninja's behavior. Signed-off-by: Fredrik Foss-Indrehus --- README.md | 6 ------ build.c | 5 ++++- build.h | 2 +- samu.1 | 4 ++++ samu.c | 10 ++++++++++ util.c | 36 ++++++++++++++++++++++++++++++++++++ util.h | 3 +++ 7 files changed, 58 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d9bc61f..791528b 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,6 @@ are several cases where it is slightly different: so the first to execute depends on the address returned by `malloc`. This may result in build failures due to insufficiently specified dependencies in the project's build system. -- samurai does not post-process the job output in any way, so if it - includes escape sequences they will be preserved, while ninja strips - escape sequences if standard output is not a terminal. Some build - systems, like meson, force color output from gcc by default using - `-fdiagnostics-color=always`, so if you plan to save the output to a - log, you should pass `-Db_colorout=auto` to meson. - samurai follows the [POSIX Utility Syntax Guidelines], in particular guideline 9, so it requires that any command-line options precede the operands. It does not do GNU-style argument permutation. diff --git a/build.c b/build.c index 887ba33..6a782d2 100644 --- a/build.c +++ b/build.c @@ -469,8 +469,11 @@ jobdone(struct job *j) j->failed = true; } close(j->fd); - if (j->buf.len && (!consoleused || j->failed)) + if (j->buf.len && (!consoleused || j->failed)) { + if (!buildopts.color) + stripansi(&j->buf); fwrite(j->buf.data, 1, j->buf.len, stdout); + } j->buf.len = 0; e = j->edge; if (e->pool) { diff --git a/build.h b/build.h index 47d6ea3..a0075ec 100644 --- a/build.h +++ b/build.h @@ -2,7 +2,7 @@ struct node; struct buildoptions { size_t maxjobs, maxfail; - _Bool verbose, explain, keepdepfile, keeprsp, dryrun; + _Bool verbose, explain, keepdepfile, keeprsp, dryrun, color; const char *statusfmt; double maxload; }; diff --git a/samu.1 b/samu.1 index d2f711e..7672690 100644 --- a/samu.1 +++ b/samu.1 @@ -245,6 +245,10 @@ Elapsed time in seconds (to 3 decimal places). .It Cm %% The '%' character. .El +.It Ev CLICOLOR_FORCE +If set to a value other than "0", force ANSI color escape codes to be printed even if standard output is not a terminal. +.It Ev TERM +If set to "dumb", disable ANSI color escape codes. .El .Sh SEE ALSO .Xr make 1 diff --git a/samu.c b/samu.c index 0c389bc..75ae26a 100644 --- a/samu.c +++ b/samu.c @@ -3,6 +3,7 @@ #include #include #include +#include #include "arg.h" #include "build.h" #include "deps.h" @@ -145,6 +146,8 @@ main(int argc, char *argv[]) struct node *n; long num; int tries; + const char *term, *clicolor_force; + argv0 = progname(argv[0], "samu"); parseenvargs(getenv("SAMUFLAGS")); @@ -218,6 +221,13 @@ main(int argc, char *argv[]) if (!buildopts.statusfmt) buildopts.statusfmt = "[%s/%t] "; + term = getenv("TERM"); + clicolor_force = getenv("CLICOLOR_FORCE"); + + buildopts.color = isatty(1) && term && strcmp(term, "dumb") != 0; + if (!buildopts.color) + buildopts.color = clicolor_force && strcmp(clicolor_force, "0") != 0; + setvbuf(stdout, NULL, _IOLBF, 0); tries = 0; diff --git a/util.c b/util.c index 8ee3ddd..522ca86 100644 --- a/util.c +++ b/util.c @@ -225,3 +225,39 @@ writefile(const char *name, struct string *s) return ret; } + +static int +islatinalpha(int c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +void +stripansi(struct buffer *buf) +{ + char *read = buf->data; + char *write = buf->data; + char *end = buf->data + buf->len; + + if (!buf->data) + return; + + while (read < end) { + if (*read != '\033') { + *write++ = *read++; + continue; + } + if (read + 1 >= end) + break; + if (*(read + 1) != '[') { + read++; + continue; + } + read += 2; + while (read < end && !islatinalpha(*read)) + read++; + if (read < end) + read++; + } + buf->len = write - buf->data; +} diff --git a/util.h b/util.h index 5da348c..aa4eb84 100644 --- a/util.h +++ b/util.h @@ -40,3 +40,6 @@ void delevalstr(void *); void canonpath(struct string *); /* write a new file with the given name and contents */ int writefile(const char *, struct string *); + +/* strip ANSI escape codes from buffer in-place */ +void stripansi(struct buffer *buf);