ax-check rule

AXC-C010: ANSI colour codes without a terminal

Output contained ANSI escape sequences although stdout and stderr were not terminals.

ax-check is a checker being prepared for release. This page documents the rule ahead of that release; see all 50 rules.

Severitywarn
KindObserved. Depends on the program and environment at run time.
Modeax-check probe-cli
Applies toCommand-line programs, run as a subprocess
Pattern tagsnon-interactive, context-budget
Fix in one lineOnly colour output when the stream is a TTY; check each stream separately.

The output contained ANSI escape sequences even though stdout and stderr were pipes, not terminals. Those sequences are invisible on a screen but show up as noise when a program or an agent reads the raw bytes.

What it checks

The rule scans the output of every probe that ran, except the no-color probe, for ANSI escape sequences, such as ESC[31m for colour or ESC[2K for erasing a line. It produces one finding, which lists every probe that emitted one. Every probe runs with no TTY, with CI=1 set, and with FORCE_COLOR, CLICOLOR_FORCE and NO_COLOR removed from the environment. This means ax-check has not asked for colour in any way.

Why it matters

Raw escape codes break string matching, add characters to every line and make parsed output harder to read. An agent that reads \x1b[31merror\x1b[0m must work out that the extra characters are decoration. They also take space in the model context for no gain. The Command Line Interface Guidelines say to disable colour when “stdout or stderr is not an interactive terminal (a TTY)”, when NO_COLOR is set, when TERM is dumb, or with --no-color.

How to fix

Colour only when the stream is a TTY, and check stdout and stderr separately, since one can be a terminal while the other is a pipe. Most colour libraries do this for you, but only if you ask them to detect the stream and not force colour on.

Example

Before

$ invoicer list --status bogus </dev/null 2>&1 | cat -v
^[[31merror^[[0m: unknown status "bogus"
^[[2mValid values: open, paid, void^[[0m

After

$ invoicer list --status bogus </dev/null 2>&1 | cat -v
error: unknown status "bogus"
Valid values: open, paid, void

A Node fix:

const useColour = process.stderr.isTTY && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
const red = (s) => (useColour ? `\x1b[31m${s}\x1b[0m` : s);
console.error(`${red('error')}: unknown status "bogus"`);

A Python fix:

use_colour = sys.stderr.isatty() and not os.environ.get("NO_COLOR") and os.environ.get("TERM") != "dumb"

How ax-check detects it

ax-check searches the captured stdout and stderr of each probe for any ANSI escape sequence: ESC [, then parameters, then a final character. It does not decode what the sequence does, so cursor movement and line erasing count as well as colour. That is deliberate: none of these belong in piped output. AXC-C011 is narrower and checks colour codes only.

The no-color probe is not scanned here, because AXC-C011 covers it. If your program colours only when FORCE_COLOR is set, it passes here because ax-check removes that variable.

To reproduce by hand: invoicer --help </dev/null 2>&1 | cat -v | grep -n '\^\[\['.

Known false positives: tools whose purpose is to produce coloured output for later display. Silence the rule with --disable AXC-C010 for those.

Safety note: This rule scans every probe that ran; by default, only the help and version probes. probe-cli executes your program under your own OS account. The temporary working directory and HOME it uses are not a sandbox: the program can still read and write anything your account can, and use the network. Probe a CLI you have not reviewed only inside a container or a throwaway virtual machine. Run ax-check probe-cli --dry-run -- invoicer first to see the probes that would run, and see the “Probe safety” section of the ax-check README for what is and is not isolated.

Sources

  • Guideline: Command Line Interface Guidelines, clig.dev. https://clig.dev/ . Lists the cases where colour should be disabled: stdout or stderr is not a TTY, NO_COLOR is set, TERM is dumb, or --no-color is passed.
  • Specification: NO_COLOR, no-color.org. https://no-color.org/ . Says software that adds ANSI colour by default should check for NO_COLOR.

Records in the AX evidence register that share a pattern tag with this rule. A shared tag means the record is about the same pattern, not that it tests this rule. Read the evidence class before the number.