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?
answer
- two channels, product and commentary
- the number before > defaults to 1
- ampersand means descriptor, not filename
- so $( ) capture stays clean
- message on stderr, status non-zero
basics
~20 sThe >&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#!/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
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.
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.
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.
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