bash: exit codesExit codes, $?, set -euo pipefail and trap
Every command ends with an exit status from 0 to 255: 0 means success, anything else a kind of failure. $? holds the last one, if and && read it, and CI systems, systemd and Kubernetes decide pass or fail from it. set -euo pipefail and trap make a script stop on the first failure and say where.
cmd; echo $? exit N set -euo pipefail trap 'handler' ERR EXIT ${PIPESTATUS[@]}Before trusting exit codes in automation, see what common commands actually return in each situation.
set -eerrexit: if a command fails (outside if, while, && or ||), the script exits immediately with that status.
When a process ends, by calling exit(N) or returning from main, the kernel keeps its status in the process table (the process is a zombie) until the parent collects it with waitpid(). The status word holds either the low 8 bits of N, or the number of the signal that killed it. Bash turns that into $?: N for a normal exit, 128 + signal number for a kill (SIGKILL 9 gives 137, SIGTERM 15 gives 143), and it uses 126 for 'found but not executable' and 127 for 'command not found'. So exit 256 becomes 0. For a pipeline, $? is the status of the last command; PIPESTATUS has them all and pipefail makes any failure count. set -e checks the status after each simple command and exits when it is non-zero, but deliberately not for commands tested by if, while, until, && or ||, or negated with !, which is why it has surprising gaps. When the script ends, its own last status goes back to whatever started it: your shell, a CI runner, systemd or the kubelet.
A child's status travels through the kernel to bash's $?, and from the script to whatever started it.
The runner starts bash backup.sh as a child process and will judge the job only by the status it gets back.
$?Exit status of the last command (read it immediately: the next command overwrites it)exit NEnd the script with status N (0-255); exit alone uses the last status0 / 1 / 2Success / general failure / misuse (grep: 0 match, 1 no match, 2 error)126 / 127Found but not executable / command not found128+NKilled by signal N: 130 Ctrl-C, 137 SIGKILL (OOM), 143 SIGTERMset -eExit when a command fails (with known exceptions)set -uError on unset variablesset -o pipefailA pipeline's status is the last non-zero status in it${PIPESTATUS[@]}Status of every command in the last pipelinetrap '...' ERRRun a handler when a command fails (add set -E to cover functions)trap '...' EXITAlways run cleanup when the script ends, success or notcmd || trueDeliberately ignore one failure under set -eScripts without set -e or explicit checksA failed cd or download is ignored, later commands run in the wrong state, and the script still exits 0 because its last echo worked.
set -euo pipefail at the top, or cmd || die "message" on every step that matterssome_cmd; if [ $? -eq 0 ]; thenWorks, but anything inserted between the two lines (even a debug echo) silently changes $?.
if some_cmd; thenTrusting set -e inside if, && chains or functions called from themset -e is suspended for any command whose status is being tested, including everything inside a function called as if f; then.
Check critical steps explicitly: step || return 1curl -s "$url" | jq .version in a deploy checkWithout pipefail a failed download is hidden: jq gets no input and the pipeline returns jq's status.
set -o pipefail; curl -fsS "$url" | jq -e .versionexit -1 or exit 500 for custom errorsStatuses are 0-255: -1 becomes 255, 500 becomes 244. Codes above 125 look like signals or shell errors.
Use 1 for general failure and small distinct codes (2, 3, 64) for specific casesTry these on a Linux machine of your own. Each task has a hint and a solution.
Run grep for a word that is in /etc/passwd, one that isn't, and against a missing file, printing $? after each.
Show the exit status of each stage of: cat /nope | sort | wc -l
Start sleep 100 in the background, kill it with SIGKILL, and print the exit status bash reports.
Write a script that creates a temp directory, always removes it on exit, and then fails on purpose with exit 3. Check the directory is gone.
bash: exit codesExit codes, $?, set -euo pipefail and trap
Every command ends with an exit status from 0 to 255: 0 means success, anything else a kind of failure. $? holds the last one, if and && read it, and CI systems, systemd and Kubernetes decide pass or fail from it. set -euo pipefail and trap make a script stop on the first failure and say where.
cmd; echo $? exit N set -euo pipefail trap 'handler' ERR EXIT ${PIPESTATUS[@]}Before trusting exit codes in automation, see what common commands actually return in each situation.
set -eerrexit: if a command fails (outside if, while, && or ||), the script exits immediately with that status.
When a process ends, by calling exit(N) or returning from main, the kernel keeps its status in the process table (the process is a zombie) until the parent collects it with waitpid(). The status word holds either the low 8 bits of N, or the number of the signal that killed it. Bash turns that into $?: N for a normal exit, 128 + signal number for a kill (SIGKILL 9 gives 137, SIGTERM 15 gives 143), and it uses 126 for 'found but not executable' and 127 for 'command not found'. So exit 256 becomes 0. For a pipeline, $? is the status of the last command; PIPESTATUS has them all and pipefail makes any failure count. set -e checks the status after each simple command and exits when it is non-zero, but deliberately not for commands tested by if, while, until, && or ||, or negated with !, which is why it has surprising gaps. When the script ends, its own last status goes back to whatever started it: your shell, a CI runner, systemd or the kubelet.
A child's status travels through the kernel to bash's $?, and from the script to whatever started it.
The runner starts bash backup.sh as a child process and will judge the job only by the status it gets back.
$?Exit status of the last command (read it immediately: the next command overwrites it)exit NEnd the script with status N (0-255); exit alone uses the last status0 / 1 / 2Success / general failure / misuse (grep: 0 match, 1 no match, 2 error)126 / 127Found but not executable / command not found128+NKilled by signal N: 130 Ctrl-C, 137 SIGKILL (OOM), 143 SIGTERMset -eExit when a command fails (with known exceptions)set -uError on unset variablesset -o pipefailA pipeline's status is the last non-zero status in it${PIPESTATUS[@]}Status of every command in the last pipelinetrap '...' ERRRun a handler when a command fails (add set -E to cover functions)trap '...' EXITAlways run cleanup when the script ends, success or notcmd || trueDeliberately ignore one failure under set -eScripts without set -e or explicit checksA failed cd or download is ignored, later commands run in the wrong state, and the script still exits 0 because its last echo worked.
set -euo pipefail at the top, or cmd || die "message" on every step that matterssome_cmd; if [ $? -eq 0 ]; thenWorks, but anything inserted between the two lines (even a debug echo) silently changes $?.
if some_cmd; thenTrusting set -e inside if, && chains or functions called from themset -e is suspended for any command whose status is being tested, including everything inside a function called as if f; then.
Check critical steps explicitly: step || return 1curl -s "$url" | jq .version in a deploy checkWithout pipefail a failed download is hidden: jq gets no input and the pipeline returns jq's status.
set -o pipefail; curl -fsS "$url" | jq -e .versionexit -1 or exit 500 for custom errorsStatuses are 0-255: -1 becomes 255, 500 becomes 244. Codes above 125 look like signals or shell errors.
Use 1 for general failure and small distinct codes (2, 3, 64) for specific casesTry these on a Linux machine of your own. Each task has a hint and a solution.
Run grep for a word that is in /etc/passwd, one that isn't, and against a missing file, printing $? after each.
Show the exit status of each stage of: cat /nope | sort | wc -l
Start sleep 100 in the background, kill it with SIGKILL, and print the exit status bash reports.
Write a script that creates a temp directory, always removes it on exit, and then fails on purpose with exit 3. Check the directory is gone.