Skip to content
Select theme

How to detect whether output is a terminal

Short answer: Call io::stdout().is_terminal() — the method comes from the std::io::IsTerminal trait, stable since Rust 1.70 — and let the answer decide whether escape codes are printed at all.

From a fresh project — cargo new is-a-terminal — replace src/main.rs with each block below and run it with cargo run -q. Piping the output through cat is the interesting case: a pipe is not a terminal, and the program can tell.

$ cargo run -q | cat
stdout is a terminal: false
stderr is a terminal: false
stdin is a terminal: false

Every stream answers for itself. In a real terminal all three read true, and a pseudo-terminal is enough to flip them. No interactive session is needed; python3 can open one on macOS and Linux alike:

$ python3 -c 'import pty; pty.spawn(["cargo", "run", "-q"])' | tr -d '\r'
stdout is a terminal: true
stderr is a terminal: true
stdin is a terminal: true

A pty turns each newline into \r\n, and tr -d '\r' puts them back. On Windows the same call answers for consoles, and for msys and cygwin pseudo-terminals as well.

Colour codes are terminal instructions. Written into a pipe they are raw escape bytes sitting in the middle of the text, which is what the previous article’s NO_COLOR-only check produces when the output is piped:

$ cargo run -q | cat -v
^[[32mok^[[0m done

cat -v shows ESC as ^[. That line looks right in a terminal and wrong everywhere else, so the terminal check joins the environment check: one && is all it takes.

$ cargo run -q | cat -v
ok done
$ python3 -c 'import pty; pty.spawn(["cargo", "run", "-q"])' | tr -d '\r' | cat -v
^[[32mok^[[0m done
$ NO_COLOR=1 python3 -c 'import pty; pty.spawn(["cargo", "run", "-q"])' | tr -d '\r' | cat -v
ok done

Three environments, three answers. Read NO_COLOR once at startup and pass the flag down, as the previous article puts it; the terminal check is one call at the top of main, not one per write.

stdout and stderr answer separately, and a CLI sends them to different places: results to stdout, progress and diagnostics to stderr. Any handle answers, not just the standard three — a File you opened yourself reports false, which is what keeps a log file clean. Redirect stdout alone and the split shows:

$ python3 -c 'import pty; pty.spawn(["sh", "-c", "cargo run -q > out.txt"])' | tr -d '\r'
stderr is a terminal: true
$ cat out.txt
stdout is a terminal: false

So mycli | tee log.txt keeps stderr on the terminal while stdout goes to the file. A progress bar that checks stdout goes dark in exactly that case, while the user is still watching the terminal; check the stream the bar is drawn on. The next step in this chapter adds a progress bar, and that decision is what it turns on.

true means a terminal is attached, not that the terminal can do colour: TERM=dumb, an eight-colour xterm and a truecolor terminal all answer true. The same trait answers the question for input, which is what the probe’s third line is for: a prompt behind io::stdin().is_terminal() stays quiet when input arrives from a pipe.