stdin, stdout, stderr, and File Descriptors
Bash becomes a powerful automation language when you stop thinking only in terms of commands and start thinking in terms of data streams. Every command participates in a small I/O contract: where input comes from, where normal output goes, where diagnostics go, and what exit status tells the caller.
Learning objectives
By the end of this lesson
- Define standard input, standard output, and standard error.
- Explain file descriptors 0, 1, and 2 and how processes inherit them.
- Distinguish command output from command exit status.
- Inspect whether a stream is connected to a terminal, file, or pipeline.
- Design command-line automation that keeps machine-readable output separate from diagnostics.
1. Every process starts with an I/O contract
On Unix-like systems, a newly started process normally receives three already-open file descriptors. Bash and the programs it launches use these descriptors as conventional channels for input and output.
stdinStandard inputFile descriptor 0; data a program reads by defaultstdoutStandard outputFile descriptor 1; normal program outputstderrStandard errorFile descriptor 2; diagnostics and error messagesA file descriptor is a small integer that a process uses to refer to an open I/O resource. The resource does not have to be a regular file. It can represent a terminal, pipe, socket, device, or other kernel-managed object.
flowchart LR T["Terminal keyboard"] -->|"fd 0 / stdin"| P["Process"] P -->|"fd 1 / stdout"| O["Terminal display"] P -->|"fd 2 / stderr"| E["Terminal display"]
2. stdout and stderr exist for different audiences
Normal output is often intended for another program. Diagnostics are intended to explain what the program is doing or why it failed. Keeping them separate lets automation consume clean data while operators still receive useful errors.
# stdout carries the requested data.
printf 'artifact-2026.08.09.tar.gz\n'
# stderr carries a diagnostic.
printf 'warning: cache was unavailable\n' >&2If a script emits JSON on stdout, logging warnings into the same stream can make the JSON invalid. A production-friendly command therefore treats stdout as an interface, not merely as a place to print text.
If another command, CI step, or API wrapper will parse stdout, keep human diagnostics on stderr.
3. Bash redirection syntax operates on file descriptors
The expression >&2 means “send this command's standard output to the destination currently referenced by descriptor 2.” The left side defaults to descriptor 1 when omitted.
printf 'normal output\n'
printf 'diagnostic output\n' >&2
# The long form is equivalent for stdout:
printf 'diagnostic output\n' 1>&2Redirection is processed by the shell before the target command executes. The program usually does not need to know whether its stdout points at a terminal, file, or pipe.
4. Child processes inherit open descriptors
When Bash launches a child process, the child normally inherits Bash's open standard descriptors unless redirections replace them. This is why one shell-level redirection can affect an entire command invocation.
flowchart TB B["Bash"] -->|"launch child"| C["Child process"] B -. "fd 0" .-> I["input source"] B -. "fd 1" .-> O["output destination"] B -. "fd 2" .-> E["diagnostic destination"] C -. "inherits / redirected" .-> I C -. "inherits / redirected" .-> O C -. "inherits / redirected" .-> E
5. Exit status is not another output stream
A command's exit status is a small integer reported to its parent when the command terminates. It is not printed on stdout or stderr unless some program explicitly prints it.
grep 'needle' /etc/hosts
status=$?
printf 'grep exit status = %d\n' "$status" This separation is fundamental:
- stdout can contain a result.
- stderr can contain diagnostics.
- exit status tells the caller whether the command considered the operation successful.
A command can produce no output and still succeed. It can also print useful output and later fail.
6. Programs can behave differently when attached to a terminal
The test -t FD condition checks whether a file descriptor refers to a terminal. Many CLI programs use terminal detection to decide whether to enable colors, progress bars, prompts, or interactive formatting.
for fd in 0 1 2; do
if [[ -t $fd ]]; then
printf 'fd %d is a terminal\n' "$fd"
else
printf 'fd %d is not a terminal\n' "$fd"
fi
doneTry the command normally, then redirect its stdout to a file. Descriptor 1 will stop being a terminal. This explains why some commands automatically change formatting inside CI logs or pipelines.
7. Linux exposes descriptors through /proc
On Linux, a process's open descriptors can be inspected through /proc/PID/fd. For the current Bash process, $$ is its process ID.
ls -l "/proc/$$/fd" 2>/dev/null || printf '/proc descriptor inspection is not available here\n'You may see symbolic links for descriptors 0, 1, and 2 pointing to a terminal device, pipe, or other resource. This inspection is Linux-specific; the underlying descriptor concept is broader than Linux.
8. Design scripts with a clean stream contract
Suppose a script must return one artifact path for a downstream job while also describing progress to humans. Put the path on stdout and progress on stderr:
#!/usr/bin/env bash
artifact="/tmp/build/app.tar.gz"
printf 'building artifact...\n' >&2
# build steps would run here
printf 'build complete\n' >&2
# Machine-readable result:
printf '%s\n' "$artifact" A caller can then capture only the result:
artifact=$(bash build.sh)
printf 'deploying %s\n' "$artifact" Command substitution captures stdout. stderr normally remains connected to the caller's stderr, which makes this stream contract especially useful.
9. Bash can use descriptors beyond 0, 1, and 2
Scripts sometimes allocate additional descriptors for logs, lock files, or separate channels. Modern Bash can choose an unused descriptor automatically.
log_file=$(mktemp)
exec {logfd}>"$log_file"
printf 'allocated descriptor: %s\n' "$logfd"
printf 'structured log line\n' >&"$logfd"
exec {logfd}>&-
cat "$log_file"
rm -f "$log_file" The exec builtin changes the current shell's descriptor table when it is used with redirections and no external command. This is powerful, but keep descriptor lifetimes explicit and close resources you no longer need.
10. Hands-on lab: prove stream separation
Create a script that intentionally writes one record to stdout and three diagnostics to stderr. Capture the streams separately and verify the exit status independently.
mkdir -p "$HOME/devops-academy/bash/chapter03/lesson01"
cd "$HOME/devops-academy/bash/chapter03/lesson01"
cat > stream-demo.sh <<'EOF'
#!/usr/bin/env bash
printf 'starting lookup\n' >&2
printf 'checking local cache\n' >&2
printf '{"service":"api","status":"ready"}\n'
printf 'lookup complete\n' >&2
exit 0
EOF
bash stream-demo.sh >result.json 2>diagnostics.log
status=$?
printf 'exit=%d\n' "$status"
printf '%s\n' '--- stdout ---'
cat result.json
printf '%s\n' '--- stderr ---'
cat diagnostics.logVerification checklist
11. Knowledge check
Question 1. Which descriptor conventionally represents stderr?
Question 2. Why should a CLI that emits JSON avoid progress messages on stdout?
Question 3. Is exit status carried through fd 2?
12. Summary
Standard streams are the foundation of composable Unix automation. Descriptor 0 is stdin, 1 is stdout, and 2 is stderr. Bash can reconnect those descriptors before launching commands, allowing files, terminals, pipes, and processes to be combined without modifying the programs themselves. Preserve the distinction between normal output, diagnostics, and exit status.
13. Further reading
- GNU Bash Reference Manual — Redirections.
- POSIX Shell Command Language — redirection and command execution.
- Linux
open(2),dup2(2), andproc(5)manual pages. - GNU Coreutils documentation — standard input and output conventions.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.