Bash scripting guide · part 18 of 32
Exit codes
What exit statuses mean, how $? works, the reserved values 126, 127 and 128+N, PIPESTATUS, and how to choose codes for your own scripts.
Every command finishes with an exit status, a number from 0 to 255. Zero means success;
anything else means some kind of failure, and the command decides which number means what.
The shell's whole control flow (if, while, &&, ||, set -e) is built on this one
number, so knowing how it behaves is most of knowing how to write reliable scripts.
Reading $?
$? holds the status of the most recent command. "Most recent" is literal: an echo that
prints $? is itself a command, so the next $? is the echo's status.
$? is replaced by every command
false
echo "false returned $?"
echo "and that echo returned $?"
false returned 1 and that echo returned 0
false exits 1. The first echo succeeds, so the second one sees 0. Save the value right
away (rc=$?) if you need it later.
Statuses carry meaning
Many commands use more than two values. grep is the classic: 0 means a line matched,
1 means it ran fine but nothing matched, 2 means an error such as a missing file. Testing
if grep ... treats "no match" and "file missing" the same way, which is not always what you want.
Three outcomes from grep
grep -q ERROR logs/app.log; echo "match: $?"
grep -q PANIC logs/app.log; echo "no match: $?"
grep -q ERROR logs/missing.log; echo "error: $?"
match: 0 no match: 1 grep: logs/missing.log: No such file or directory error: 2
The log contains ERROR, so the first call returns 0. PANIC does not occur, so the second
returns 1. The third file does not exist: grep prints a message and returns 2.
diff and cmp follow the same convention (0 same, 1 different, 2 trouble). test
returns 0 for true and 1 for false. Check a command's man page before you assume
"non-zero means broken".
Codes the shell reserves
Some values are produced by the shell itself rather than by the program:
| Status | Meaning |
|---|---|
| 126 | The file was found but could not be executed (no execute permission, or it is a directory) |
| 127 | Command not found |
| 128+N | The command was killed by signal N (130 = Ctrl-C/SIGINT, 137 = SIGKILL, 143 = SIGTERM) |
| 2 | Bash builtins use it for usage errors, and bash for syntax errors |
127, 126 and 128+N in practice
no-such-command; echo "status $?"
printf 'echo hi\n' > notexec.sh
./notexec.sh; echo "status $?"
sleep 30 & kill $!; wait $!; echo "status $?"
/bin/bash: line 1: no-such-command: command not found status 127 /bin/bash: line 3: ./notexec.sh: Permission denied status 126 status 143
An unknown command gives 127. A file without the execute bit gives 126 with "Permission
denied". The background sleep was sent SIGTERM (signal 15), and wait reports 128 + 15 = 143.
Setting your own status
A script's status is the status of the last command it ran, unless it calls exit N. A
function's is its last command's, unless it calls return N. Only the low 8 bits are kept,
so exit 256 is success and exit -1 is 255. Stay within 0 to 125 for your own codes so
they never collide with the shell's.
exit and return without a number pass on the status of the last command that ran. That
makes cd "$dir" || exit a complete error check: if cd fails with 1, the script exits with 1.
Exit statuses wrap at 256
bash -c 'exit 3'; echo "exit 3 -> $?"
bash -c 'exit 256'; echo "exit 256 -> $?"
bash -c 'exit -1'; echo "exit -1 -> $?"
exit 3 -> 3 exit 256 -> 0 exit -1 -> 255
3 comes through as is. 256 wraps to 0, so a script that counts errors and does exit $errors
reports success after exactly 256 failures. -1 becomes 255. Cap such counts, or exit 1.
A script without exit returns its last command's status
#!/usr/bin/env bash
echo "checking for the config file"
test -f data/missing.conf
bash check.sh; echo "script status: $?"
checking for the config file script status: 1
The script never calls exit, so its status is whatever test -f returned: 1, because the
file does not exist. Adding a trailing echo done would silently turn it into 0.
Acting on a status
Most of the time you do not read $? at all. if cmd runs cmd and branches on its
status; cmd1 && cmd2 runs cmd2 only on success; cmd1 || cmd2 only on failure; !
inverts a status. Testing $? explicitly is only worth it when you need to tell several
non-zero values apart, and then a case is clearer than a chain of ifs.
Branch on a status directly or inspect the value
if grep -q 'TODO:' notes.txt; then echo "notes have a TODO"; fi
! grep -q PANIC logs/app.log && echo "no panics logged"
grep -q x nofile 2>/dev/null
case $? in
0) echo found ;;
1) echo "not found" ;;
*) echo "grep failed" ;;
esac
notes have a TODO no panics logged grep failed
The if and the ! form never touch $?. The case captures the status of the third grep,
which was 2 because nofile does not exist, so it reports a failure rather than "not found".
Pipelines
A pipeline's status is the status of its last command. Earlier failures are invisible
unless you look at PIPESTATUS, an array with one status per stage, or turn on
set -o pipefail (covered in Strict mode and its caveats). Like $?, PIPESTATUS is overwritten by
the next command, so copy it first.
Find out which stage of a pipeline failed
cat no-such-file | sort | wc -l
echo "status=$? stages=${PIPESTATUS[*]}"
cat no-such-file | sort | wc -l
codes=("${PIPESTATUS[@]}")
echo "cat=${codes[0]} sort=${codes[1]} wc=${codes[2]}"
cat: no-such-file: No such file or directory 0 status=0 stages=1 0 0 cat: no-such-file: No such file or directory 0 cat=1 sort=0 wc=0
cat failed with 1, but wc happily counted zero lines and exited 0, so $? says the
pipeline succeeded. Reading $? and PIPESTATUS in the same echo works because both are
expanded before the echo runs; the second run copies the array into codes first, which is
the safe habit when you need the values over several lines.
Statuses that get lost
Several constructs report their own status and hide the inner one. local x=$(cmd) and
export x=$(cmd) return the status of local or export, which is 0. A plain assignment
x=$(cmd) does keep cmd's status.
local hides a command substitution's failure
f() {
local a=$(false); echo "local a=\$(false): $?"
local b; b=$(false); echo "b=\$(false) on its own line: $?"
}
f
local a=$(false): 0 b=$(false) on its own line: 1
Declaring and assigning in one local statement reports local's own success, 0. Splitting
the declaration from the assignment lets the failure through as 1.
Choosing codes for your scripts
For most scripts two values are enough: 0 and 1. Use 2 for "you called me wrong" to match
builtins and many utilities. If callers really need to tell failures apart, document a few
codes at the top of the script and keep them below 126. /usr/include/sysexits.h defines a
set starting at 64 (EX_USAGE 64, EX_NOINPUT 66, EX_UNAVAILABLE 69, EX_CONFIG 78)
that some programs follow, but nothing in the shell relies on them.
Related
trueDo nothing and exit with status 0 (success).falseDo nothing and exit with status 1 (failure).testEvaluate a condition about files, strings or integers and return 0 for true or 1 for false.grepPrint the lines of files (or standard input) that match a pattern.diffCompare two files or directories line by line and print the changes that turn one into the other.- Strict mode and its caveatsWhat set -euo pipefail actually does, the cases where -e silently does nothing, the traps in -u and pipefail, and a balanced way to use it.
- ConditionalsHow if, elif, &&, || and ! decide using exit statuses, the three test syntaxes, guard clauses, and why
a && b || cis not an if-else. - FunctionsDefining and calling bash functions, arguments, local variables, return status versus output, namerefs, and sourcing a function library.
- Traps and signalsRunning cleanup code with trap on EXIT, handling INT and TERM, the ERR pseudo-signal, and the timing and scoping rules that trip people up.