How can a Go program tell that os.Stdout is a terminal rather than a pipe or a file?
answer
- ask the file what it is
- os.Stdout.Stat() returns an fs.FileInfo
- look at the mode's type bits
- character device versus named pipe
- /dev/null passes the same test
basics
~10 sCall 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 linesfi, 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
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.
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.
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.
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