skip to content

File Descriptors and Duplication

Redirections are dup2 calls applied left to right, which is why `>file 2>&1` and `2>&1 >file` genuinely differ. Past that trick question, exec lets you open a numbered descriptor once and keep writing to it — the standard way to give a script its own log channel.

part ofBashoverview, primer and where to startread it →
on this pageshow

questions

5

In a bash script, what does the redirection in `echo "usage: deploy <env>" >&2` do to the echo command's file descriptors, and why write error messages that way?

level: juniorimportance: must knowfreq 60%

answer

  1. two channels, product and commentary
  2. the number before > defaults to 1
  3. ampersand means descriptor, not filename
  4. so $( ) capture stays clean
  5. message on stderr, status non-zero

basics

~20 s

The >&2 is shorthand for 1>&2: it makes echo's stdout a duplicate of file descriptor 2, so the message is written to stderr instead of stdout. Diagnostics belong on stderr so a caller can capture, pipe or discard the script's real output without losing them.

solid answer

~40 s

`>&2` is `1>&2` with the default descriptor number omitted — it duplicates descriptor 2 onto descriptor 1 for that one command, so everything `echo` writes goes out on stderr. The reason to bother is composability. A script's stdout is its *product*: the thing a caller captures with `out=$(myscript)` or pipes into `jq`. Usage text, warnings and error messages are not product; if they ride on stdout they get swallowed by the capture, corrupt the piped data, or disappear into `> results.txt`. On stderr they stay visible to a human and out of the data path, and they still show up in a log when the caller says `2>> errors.log`. The convention pairs with exit status: print the diagnostic to stderr, then `exit 1`.

code

bash · 12 lines
bash
#!/usr/bin/env bash
log() { printf '%s %s\n' "$(date +%FT%T)" "$*" >&2; }
die() { log "ERROR: $*"; exit 1; }

list_hosts() {
  log "warning: cache is stale"
  printf '%s\n' web-01 web-02
}

hosts=$(list_hosts)   # warning stays on the terminal, not in $hosts
printf 'captured: %s\n' "$hosts"
[[ -n $hosts ]] || die "no hosts returned"

go deeper

for a junior

Know that 1 is stdout and 2 is stderr, that >&2 sends the message to stderr, and that the ampersand is what stops 2 being read as a filename.

for a middle

Explain it as a duplication of descriptor 2 onto descriptor 1 for that single command, and show why a function that warns on stdout corrupts a caller's $( ) capture or pipeline.

for a senior

Demonstrate the convention in a real script: a log/die helper writing to stderr, a non-zero exit paired with every message, and the observation that the caller — not the script — chooses the destination file.

for a principal

Own the contract across a fleet of scripts: stdout is machine-readable product, stderr is human commentary, exit status is the signal automation reads. Decide how that maps onto structured logs and alerting in CI and cron.

## The mechanics Every process begins with three descriptors open: 0 stdin, 1 stdout, 2 stderr. The redirection operator `n>&m` tells the shell to make descriptor `n` a duplicate of descriptor `m` — after it runs, both table entries refer to the same open terminal, file or pipe. When you leave off the number before `>`, bash supplies 1. So these two are identical: ```bash echo "boom" >&2 echo "boom" 1>&2 ``` Both mean: for this `echo` only, point fd 1 at whatever fd 2 currently refers to. `echo` is unchanged — it still calls `write()` on descriptor 1, exactly as it always does. The shell rewired the destination before `echo` ever started, and the rewiring dies with the command. The next line of the script has an untouched fd 1. Note the ampersand carefully. `echo hi >2` creates a *file called 2* in the current directory; `echo hi >&2` writes to stderr. Missing that ampersand is a common way to litter a repository with files named `2`. ## Why stdout and stderr are separated at all The split exists so a program can emit two independent things down two independent channels: - **stdout is the product.** It is what the next program in the pipeline consumes, what `$( )` captures, what `> file` archives. - **stderr is the commentary.** Progress, warnings, failures — for a human or a log, never for a parser. A script that ignores the split stops being usable as a building block: ```bash # BAD: the diagnostic rides on stdout list_hosts() { echo "warning: cache is stale" echo "web-01"; echo "web-02" } hosts=$(list_hosts) # $hosts now contains the warning as a data row ``` Send the warning to stderr and the same function composes correctly: the caller's variable holds two hostnames, and the warning is still printed on the operator's terminal. ```bash list_hosts() { echo "warning: cache is stale" >&2 echo "web-01"; echo "web-02" } ``` ## The usage-and-die idiom The canonical shape in a script that takes arguments: ```bash usage() { echo "usage: deploy <env>" >&2 exit 2 } [[ $# -eq 1 ]] || usage ``` Two things travel together: the human-readable reason on stderr, and a non-zero exit status for the machine. A caller checking `if ! deploy prod; then` learns something failed from the status; a human reading the terminal learns *what* from stderr. Printing the message with no non-zero exit is the mistake to avoid — automation cannot see text, only status. A small nuance worth knowing: `--help` explicitly requested by the user is output the user asked for, so many tools print help on stdout with status 0, and print the shorter *usage* line on stderr with a non-zero status when the invocation was wrong. ## Making it reusable A one-line helper keeps the redirection in exactly one place: ```bash log() { printf '%s %s\n' "$(date +%FT%T)" "$*" >&2; } die() { log "ERROR: $*"; exit 1; } log "starting deploy" command -v kubectl >/dev/null || die "kubectl not found" ``` Use `printf` rather than `echo` for anything with backslashes or leading dashes — `echo`'s handling of `-e` and escapes varies between bash's builtin, `/bin/echo` and other shells, while `printf` is specified. ## Sending stdout to stderr's place, not vice versa Because `>&2` is a duplication of whatever fd 2 points at, it follows the caller's redirection automatically. If the caller runs `./deploy.sh 2>> /var/log/deploy.err`, the messages land in that file with no change to the script. That is the real payoff of writing to the descriptor instead of hardcoding a path or `/dev/tty`: the script states *which channel*, and the caller decides *where*. ## What an interviewer is checking That you know `>&2` is a descriptor duplication rather than magic error syntax, that you can say why the two channels exist, and that you instinctively pair a stderr message with a non-zero exit status.

  • What is the difference between `echo hi >2` and `echo hi >&2`?
    `>&2` duplicates descriptor 2 onto descriptor 1, sending the text to stderr. `>2` has no ampersand, so `2` is read as a filename: bash creates or truncates a file literally named `2` in the current directory and writes there. It is a silent typo — nothing errors, the message just vanishes from the terminal.
  • If the script writes diagnostics with `>&2`, how does a caller collect them in a file?
    The caller redirects descriptor 2 themselves, for example `./deploy.sh prod > result.txt 2>> deploy.err`, or merges both with `> all.log 2>&1`. Because the script only says *which channel*, not which file, the destination stays the caller's decision and needs no change inside the script.
  • Should a script's `--help` output also go to stderr?
    Usually not. Help the user explicitly asked for is requested output, so print it on stdout and exit 0 — that lets `myscript --help | less` work. Reserve stderr plus a non-zero status for the short usage line printed when the invocation itself was wrong.

saying these in an interview costs you the question

  • Thinking >&2 writes to a file named 2
  • Putting warnings on stdout and breaking $( ) capture
  • Believing stderr is only for crashes, not warnings
  • Printing an error message but still exiting 0
  • Assuming stderr is always the terminal

context

open as a page

In a bash script, why does `cmd 2>&1 > out.log` behave differently from `cmd > out.log 2>&1`, and where does stderr end up in each case?

level: middleimportance: must knowfreq 72%

basics

~20 s

Bash applies redirections left to right, and each one copies where the target points at that moment. cmd > out.log 2>&1 puts stdout in the file and then makes stderr a copy of it, so both are captured. cmd 2>&1 > out.log copies stderr to the terminal first, then moves only stdout.

open as a page

A bash script must write progress lines to /var/log/deploy.log while its ordinary stdout still goes wherever the caller pointed it. How do you open a dedicated file descriptor once with exec, write to it, and close it when done?

level: middleimportance: should knowfreq 40%

basics

~20 s

Run exec 3>/var/log/deploy.log once near the top of the script. That opens the file on descriptor 3 for the whole shell, so later commands write to it with >&3 while stdout and stderr are untouched. Close it with exec 3>&- when finished.

open as a page

In a bash script, how do you send every command in one section to a log file and then restore stdout to where it was before, and when is redirecting a `{ ...; }` block the better choice?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Save the current stdout on a spare descriptor first: exec 3>&1, then exec 1>run.log, and restore afterwards with exec 1>&3 3>&-. Without the saved copy the original destination is unrecoverable. Redirecting a { ...; } group instead is safer because the shell restores stdout automatically when the group ends.

open as a page

Running `ssh host /opt/bin/start-agent.sh` never returns to your prompt even though the script finished and printed its last line. The script leaves a background process running. How do inherited file descriptors explain the hang, and what redirection fixes it?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

The background process inherited copies of the script's stdout and stderr, which are the pipes back to the ssh client. ssh keeps reading until every writer closes, so it waits on the still-running child. Redirect the child's descriptors away from those pipes when starting it: cmd >/dev/null 2>&1 </dev/null &.

open as a page