Exit Status & Chaining: How Commands Report Success
- Explain that every command returns a numeric exit status: 0 means success, non-zero means failure
- Read the last command's status from $? and recognize the common codes (0, 1, 2, 126, 127, 130, 137)
- Chain commands deliberately with ;, &&, and || based on success or failure
- Know that a pipeline's status is its LAST command's, and reach for set -o pipefail when that hides a failure
Every command answers one hidden question: did it work?
See that each command leaves behind a number saying whether it succeeded.
When a command finishes, it hands the shell a single number — its exit status (or exit code). The rule is fixed and, at first, backwards-feeling:
- `0` means success ("zero problems").
- any non-zero value means failure — and the specific number often says how it failed.
That one number is the foundation everything else in this lesson is built on.
if,&&,||, and every shell script's error handling are all just reactions to the exit status. You usually don't see it — it's returned quietly — but it's always there.This matters the moment you stop typing commands one at a time and start combining them. A script that copies a file and then deletes the original must know whether the copy actually worked before it removes anything — and "did it work?" is precisely the exit status. Ignore it and you get the classic disasters: deleting the source after a copy that silently failed, deploying after a build that errored,
cd-ing into a directory that was never created. Reading and reacting to the status is what turns a list of commands into something trustworthy.Reading the status: $? and what the numbers mean
Inspect the last command's exit status and interpret the common codes.
The shell stores the most recent command's exit status in the special variable `$?`. Check it immediately after the command — the next command overwrites it.
A few codes are worth recognizing on sight:
- `0` success · `1` generic failure · `2` misuse (bad options/syntax)
- `126` found but not executable (permission) · `127` command not found
- `130` killed by Ctrl-C · `137` killed (128 + signal 9,
SIGKILL)
The pattern for signals is
128 + signal number, which is why a killed process shows up as137or143rather than something tidy.ls /etc/hostname; echo "status: $?"/etc/hostname status: 0
ls succeeded, so it returned 0, and $? reports it. Note the echo has to come right after — run any other command first and $? would hold THAT command's status instead. ls /nope 2>/dev/null; echo "status: $?"status: 2
ls failed (no such directory) and returned a non-zero status — here 2. The failure is real even though we hid the error message with 2>/dev/null; the exit status, not the text, is how a script knows something went wrong. Check yourself
You run a command and then check echo $? and see 127. What does that almost certainly mean?
Chaining: ; then &&, ||
Compose commands that run always, only-on-success, or only-on-failure.
Once commands report success or failure, you can wire them together by that result — no
ifneeded:- `A ; B` — run A, then B, always (sequence; the results are independent).
- `A && B` — run B only if A succeeded (A's status was 0). Read it as "and then".
- `A || B` — run B only if A failed (A's status was non-zero). Read it as "or else".
These are the everyday idioms:
mkdir build && cd build(only enter the dir if it was created),command || echo 'it failed'(react to failure),make && ./run(only run if the build worked).mkdir -p /tmp/demo && cd /tmp/demo && pwd/tmp/demo
&& is a guard: each step runs only if the previous one succeeded. If mkdir failed (say, a permissions problem), the cd and pwd would never run — you don't end up operating in the wrong directory. Chaining with && is how you make a sequence safe. grep -q root /etc/passwd && echo 'has root' || echo 'no root'has root
- -q
- quiet — print nothing, just set the exit status (found=0, not found=1)
grep's exit status IS the answer: 0 if it found a match, 1 if not. So 'grep -q PATTERN file && echo yes || echo no' is a complete found/not-found test with no if-statement. -q ('quiet') suppresses grep's own output so only your echo prints. Pipelines report only their last command
Predict a pipeline's exit status and fix the case where a failure hides.
A pipeline
A | B | Chas the exit status of its last command (`C`) — by default the shell ignores whetherAorBfailed. That's usually fine, but it can hide a real problem: incurl … | grep x, ifcurlfails butgrepstill exits cleanly (finding nothing, status 1) or succeeds, you can miss that the download broke.Bash's fix is `set -o pipefail`: with it, the pipeline fails if any stage fails, reporting the rightmost non-zero status. In robust scripts you'll often see
set -euo pipefailat the top for exactly this reason.false | true; echo "without pipefail: $?"without pipefail: 0
The pipeline's status is 'true' (the last stage) = 0, even though 'false' failed upstream. By default that upstream failure is invisible to $?. That's the gotcha pipefail exists to close. set -o pipefail; false | true; echo "with pipefail: $?"; set +o pipefailwith pipefail: 1
- -o pipefail
- make a pipeline fail if ANY stage fails (report the last non-zero status)
With pipefail on, the pipeline reports the failing stage's status (1 from 'false') instead of masking it. We turn it back off afterward with set +o pipefail so the option doesn't linger in an interactive shell. Check yourself
By default, what determines the exit status of the pipeline a | b | c ?
Now do it — make the status visible
Run commands that succeed and fail so reading $? and chaining become reflex.
Three tiny experiments. First, meet the two commands whose whole job is to return a status:
true(always 0) andfalse(always 1). They print nothing — the status is the point.true; echo $?; false; echo $?Prints 0 then 1. trueandfalseexist purely to succeed or fail on demand — perfect for seeing $? change with nothing else going on. Everything about chaining is built on this success/failure signal.cat /etc/hostname && echo '--- read OK ---'; cat /no/such/file || echo '--- read FAILED, handled ---'Watch && and || pick their branch by exit status. The first cat succeeds so the && message prints; the second cat fails so the || message prints. You reacted to success and to failure without writing a single if. grep -q bash /etc/shells && echo 'bash is a valid login shell' || echo 'bash not listed'A real one-liner test: grep's exit status answers the yes/no question and the && / || turn it into a human sentence. This is the shape of countless shell checks — and now you can read exactly why it works: 0 from grep runs the &&, non-zero runs the ||.
- Every command returns an exit status: 0 = success, non-zero = failure. $? holds the most recent one (check it right away — the next command overwrites it).
- Common codes: 1 generic, 2 misuse, 126 not executable, 127 command not found, 130 Ctrl-C, 137 killed (128 + signal).
- Chain by result: A ; B (always), A && B (B only if A succeeded), A || B (B only if A failed). A && B || C is not a true if/else.
- A pipeline's status is its last command's; use set -o pipefail so an earlier stage's failure isn't hidden.
| You write | It means |
|---|---|
| echo $? | the exit status of the command that just ran |
| A && B | run B only if A succeeded (status 0) |
| A || B | run B only if A failed (status non-zero) |
| A ; B | run A, then B, regardless of A's result |
| grep -q x f && … || … | branch on whether x is in file f |
| set -o pipefail | make a pipeline fail if any stage fails |
