CrackOps
Interview PrepTracksLinuxAWS LabSoonInterview RoomSoon
Sign inStart free
CrackOps

Interview prep for the people who keep production up.

[email protected]
ProductInterview PrepTracksDashboardInterview Room
RolesDevOpsSREPlatformCloud
CompanyAboutContactPrivacyTerms
© CrackOps
Linux / Shell Scripting / bash: exit codes

bash: exit codes

Exit codes, $?, set -euo pipefail and trap

Intermediatebashscriptingerrorsci

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.

Syntaxcmd; echo $? exit N set -euo pipefail trap 'handler' ERR EXIT ${PIPESTATUS[@]}

Watch it run

Before trusting exit codes in automation, see what common commands actually return in each situation.

devops@devops-lab: ~/scripts — zsh — 96×28
➜ ~/scripts
Step 1 of 3
cat exits.sh

Each line runs one command and prints $? straight away. It must be read immediately: the echo itself sets a new $?.

Anatomy of a command

$;
optionset -e

errexit: if a command fails (outside if, while, && or ||), the script exits immediately with that status.

How it works

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.

run scriptfork + execexit(2)waitpidstoreSIGKILLCI runnerjob stepbashbackup.shtarchild processKernelprocess tableOOM killermemory limit$?last status
1/7
The CI runner starts the script

The runner starts bash backup.sh as a child process and will judge the job only by the status it gets back.

← → to step

Options you'll actually use

$?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 status
0 / 1 / 2Success / general failure / misuse (grep: 0 match, 1 no match, 2 error)
126 / 127Found but not executable / command not found
128+NKilled by signal N: 130 Ctrl-C, 137 SIGKILL (OOM), 143 SIGTERM
set -eExit when a command fails (with known exceptions)
set -uError on unset variables
set -o pipefailA pipeline's status is the last non-zero status in it
${PIPESTATUS[@]}Status of every command in the last pipeline
trap '...' ERRRun a handler when a command fails (add set -E to cover functions)
trap '...' EXITAlways run cleanup when the script ends, success or not
cmd || trueDeliberately ignore one failure under set -e

Common mistakes

Scripts without set -e or explicit checks

A 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 matters
some_cmd; if [ $? -eq 0 ]; then

Works, but anything inserted between the two lines (even a debug echo) silently changes $?.

if some_cmd; then
Trusting set -e inside if, && chains or functions called from them

set -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 1
curl -s "$url" | jq .version in a deploy check

Without 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 .version
exit -1 or exit 500 for custom errors

Statuses 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 cases

Interview questions

Practice

Try these on a Linux machine of your own. Each task has a hint and a solution.

  1. 1

    Run grep for a word that is in /etc/passwd, one that isn't, and against a missing file, printing $? after each.

  2. 2

    Show the exit status of each stage of: cat /nope | sort | wc -l

  3. 3

    Start sleep 100 in the background, kill it with SIGKILL, and print the exit status bash reports.

  4. 4

    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.

Related commands

bash: ifConditions: if, elif, else, test brackets and casebash: functionsFunctions: name a block, pass arguments, return a statuskillSend a signal to a processbash: redirectionRedirection: stdout, stderr, 2>&1, /dev/null and here-docssetShell options: set -euo pipefail, set -x and friends
Previousbash: arraysNextbash: redirection
Linux / Shell Scripting / bash: exit codes

bash: exit codes

Exit codes, $?, set -euo pipefail and trap

Intermediatebashscriptingerrorsci

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.

Syntaxcmd; echo $? exit N set -euo pipefail trap 'handler' ERR EXIT ${PIPESTATUS[@]}

Watch it run

Before trusting exit codes in automation, see what common commands actually return in each situation.

devops@devops-lab: ~/scripts — zsh — 96×28
➜ ~/scripts
Step 1 of 3
cat exits.sh

Each line runs one command and prints $? straight away. It must be read immediately: the echo itself sets a new $?.

Anatomy of a command

$;
optionset -e

errexit: if a command fails (outside if, while, && or ||), the script exits immediately with that status.

How it works

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.

run scriptfork + execexit(2)waitpidstoreSIGKILLCI runnerjob stepbashbackup.shtarchild processKernelprocess tableOOM killermemory limit$?last status
1/7
The CI runner starts the script

The runner starts bash backup.sh as a child process and will judge the job only by the status it gets back.

← → to step

Options you'll actually use

$?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 status
0 / 1 / 2Success / general failure / misuse (grep: 0 match, 1 no match, 2 error)
126 / 127Found but not executable / command not found
128+NKilled by signal N: 130 Ctrl-C, 137 SIGKILL (OOM), 143 SIGTERM
set -eExit when a command fails (with known exceptions)
set -uError on unset variables
set -o pipefailA pipeline's status is the last non-zero status in it
${PIPESTATUS[@]}Status of every command in the last pipeline
trap '...' ERRRun a handler when a command fails (add set -E to cover functions)
trap '...' EXITAlways run cleanup when the script ends, success or not
cmd || trueDeliberately ignore one failure under set -e

Common mistakes

Scripts without set -e or explicit checks

A 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 matters
some_cmd; if [ $? -eq 0 ]; then

Works, but anything inserted between the two lines (even a debug echo) silently changes $?.

if some_cmd; then
Trusting set -e inside if, && chains or functions called from them

set -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 1
curl -s "$url" | jq .version in a deploy check

Without 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 .version
exit -1 or exit 500 for custom errors

Statuses 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 cases

Interview questions

Practice

Try these on a Linux machine of your own. Each task has a hint and a solution.

  1. 1

    Run grep for a word that is in /etc/passwd, one that isn't, and against a missing file, printing $? after each.

  2. 2

    Show the exit status of each stage of: cat /nope | sort | wc -l

  3. 3

    Start sleep 100 in the background, kill it with SIGKILL, and print the exit status bash reports.

  4. 4

    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.

Related commands

bash: ifConditions: if, elif, else, test brackets and casebash: functionsFunctions: name a block, pass arguments, return a statuskillSend a signal to a processbash: redirectionRedirection: stdout, stderr, 2>&1, /dev/null and here-docssetShell options: set -euo pipefail, set -x and friends
Previousbash: arraysNextbash: redirection