skip to content

How can a Go program tell that os.Stdout is a terminal rather than a pipe or a file?

level: middleimportance: should knowfreq 40%

answer

  1. ask the file what it is
  2. os.Stdout.Stat() returns an fs.FileInfo
  3. look at the mode's type bits
  4. character device versus named pipe
  5. /dev/null passes the same test

basics

~10 s

Call os.Stdout.Stat() and inspect the returned mode: fi.Mode()&os.ModeCharDevice != 0 means stdout is a character device, which is what a terminal looks like. A pipe reports os.ModeNamedPipe and a redirected file reports neither.

solid answer

~40 s

`os.Stdout` is an `*os.File`, so `fi, err := os.Stdout.Stat()` gives an `fs.FileInfo`, and `fi.Mode()&os.ModeCharDevice != 0` is the standard-library-only test for "this looks like a terminal". A pipe sets `os.ModeNamedPipe` instead; a redirected regular file sets no device bit and `fi.Mode().IsRegular()` is true. Tools use the answer to choose presentation, never content: colour, a progress display and a padded human table when interactive; plain, stable, machine-readable output when not. Two caveats matter. It is an approximation - `/dev/null` is also a character device, so `tool > /dev/null` looks interactive - and a rigorous check needs a platform-specific terminal ioctl, which is what the `golang.org/x/term` package exists for. Also stat the stream you are actually writing to: if the progress goes to stderr, stat `os.Stderr`. And always let an explicit flag override the guess.

code

go · 12 lines
go
fi, err := os.Stdout.Stat()
if err != nil {
	return err
}
switch {
case fi.Mode()&os.ModeCharDevice != 0:
	// a terminal - or another character device, such as /dev/null
case fi.Mode()&os.ModeNamedPipe != 0:
	// piped into another command
case fi.Mode().IsRegular():
	// redirected into a file
}

go deeper

for a junior

Know that a Go program can ask os.Stdout.Stat() what kind of file it is, and that tools use the answer to decide whether to print colour or an animated progress display.

for a middle

Explain the mode bits - os.ModeCharDevice for a terminal, os.ModeNamedPipe for a pipe, no type bit for a redirected regular file - and write the bitwise test correctly, including handling the Stat error.

for a senior

Demonstrate the operational discipline: detect on the stream you actually decorate, treat the result as a default an explicit flag overrides, and never let detection change the data a script parses.

for a principal

You own the promise that piped output is stable. Decide once, across the team's tools, whether interactive niceties are opt-in or opt-out, because inconsistent defaults turn every automation script into a per-tool exception.

## Why a program asks A well-behaved command-line tool presents itself differently to a human and to a script. Interactively you want colour, aligned columns, a spinner or a percentage counter, maybe a pager-friendly width. In a pipeline you want none of that: escape codes become garbage in a log file, and a spinner's carriage returns wreck a captured transcript. So the tool asks, at startup, what its output is attached to. ## The standard-library test There is no `IsTerminal` in the standard library. What there is: `os.Stdout` is an `*os.File`, and files can be stat'd. ```go fi, err := os.Stdout.Stat() if err != nil { return err } interactive := fi.Mode()&os.ModeCharDevice != 0 ``` `Stat` returns an `fs.FileInfo`; `Mode()` returns an `fs.FileMode` whose high bits describe the kind of file. The relevant ones: - `os.ModeCharDevice` - a character device. A terminal (`/dev/pts/3`, `/dev/tty`) is one. - `os.ModeDevice` - any device; a character device sets both bits. - `os.ModeNamedPipe` - what you get under `tool | other`. - no type bits at all, and `fi.Mode().IsRegular()` true - `tool > out.txt`. So the three shell situations are distinguishable, and the same test works on `os.Stdin` (is a human typing?) and `os.Stderr` (is the diagnostic stream a terminal?). ## Stat the stream you are writing to This is the mistake worth calling out. Progress and status belong on `os.Stderr`, but the terminal check is habitually written against `os.Stdout`. The two disagree in exactly the cases that matter: `tool > out.txt` has stdout as a regular file while stderr is still the terminal, and the user very much wants to see progress. `tool 2> log` is the reverse. Whichever stream carries the decorated output is the stream to stat. ## What the answer may and may not change Change **presentation**: colour, bold, progress rendering, column padding, whether a table gets box drawing, whether a confirmation prompt is offered at all. Do not change **content**. If the piped form has different fields or a different order than the interactive form, a user who developed a command interactively gets different bytes inside a script, and that is a miserable class of bug to chase. Keep the records identical; vary only the dressing. And expose explicit flags - a `--color=always|never|auto` shape, or an output-format flag - so the detection is only ever the default. ## The limits of ModeCharDevice The check tells you the file is a character device, which is not quite "a terminal a human is watching": - `/dev/null` is a character device. `tool > /dev/null` will look interactive. - Serial ports and other devices are character devices too. - A real terminal may still be unable to render colour, which is why the surrounding conventions matter: a `TERM` environment variable of `dumb`, or a set `NO_COLOR`, should suppress escape codes regardless of the mode bits. - On Windows the notion of a console does not map onto Unix mode bits at all. The rigorous check is a platform-specific terminal ioctl, which is precisely what `golang.org/x/term` wraps. Reaching for it is reasonable in a serious CLI; `ModeCharDevice` is the dependency-free approximation and is good enough for choosing whether to animate a counter. ## Do not ignore the error `Stat` can fail - a closed or otherwise unusual descriptor. Treat a failure as "not interactive", which is the safe default: plain output never corrupts anything, whereas escape codes emitted into a log do. ## What good looks like One small function, called once at startup, returning a struct of presentation decisions: `color bool`, `progress bool`, `width int`. Flags override it. Everything downstream reads the struct rather than re-stat'ing streams in a dozen places, so behaviour is decided in exactly one place and is easy to force in tests.

  • Why should a tool change formatting but not its data when its output is not a terminal?
    Because scripts parse the piped form. If the interactive and piped outputs differ in fields or ordering, a user who debugged the command by hand gets different bytes inside their script, which is very hard to diagnose. Vary colour, progress and padding; keep the record content identical, and offer an explicit format flag so the guess can always be overridden.
  • Which mode bit does os.Stdout report when the program runs as `tool | cat`?
    `os.ModeNamedPipe`. `Stat` on descriptor 1 describes the pipe the shell created, so `fi.Mode()&os.ModeCharDevice` is zero and `fi.Mode()&os.ModeNamedPipe` is set. Under `tool > file.txt` no type bit is set at all and `fi.Mode().IsRegular()` is true, which is a second way to notice the output is being captured.
  • Is the ModeCharDevice test enough to decide that ANSI colour codes are safe?
    Not on its own. `/dev/null` and serial devices are character devices too, and a genuine terminal may still not render colour. The conventional extra checks are a `TERM` value that is not `dumb` and an unset `NO_COLOR`, and the rigorous terminal test is a platform ioctl rather than a mode bit. Treat detection as a default that a flag or environment variable can always beat.

saying these in an interview costs you the question

  • Claims the standard library has an IsTerminal function
  • Stats os.Stdout while the progress display goes to os.Stderr
  • Assumes ModeCharDevice proves a real terminal, ignoring /dev/null
  • Changes the data, not just the formatting, when output is piped
  • Ignores the error returned by os.Stdout.Stat()
  • Offers no flag to override the automatic detection