Exit Status as Bash's Fundamental Boolean Signal
Bash does not require a dedicated Boolean type to control most automation. Its fundamental truth signal is the exit status of a command: zero means success, while non-zero means failure or another non-success condition. Understanding that model makes shell control flow much easier to reason about.
Learning objectives
By the end of this lesson
- Explain why exit status is Bash's core truth signal.
- Inspect and preserve command status with $?.
- Use true, false, !, &&, and || without confusing data output with success.
- Interpret common multi-status commands such as grep.
- Design checks that distinguish expected non-matches from operational failures.
1. Bash asks whether commands succeeded
In shell programming, commands themselves participate directly in Boolean logic. Bash evaluates a command's exit status. By convention, status 0 means success; any non-zero value means some form of non-success.
flowchart LR
C["Run command"] --> S{"exit status"}
S -->|"0"| T["true / success branch"]
S -->|"non-zero"| F["false / failure branch"]true
printf 'true status=%d\n' "$?"
false
printf 'false status=%d\n' "$?"In arithmetic, zero is often described as false. For command status, zero means success and therefore acts as true in shell control flow.
2. $? contains the immediately previous status
The special parameter $? expands to the exit status of the most recently completed foreground pipeline. Because every new command can replace it, capture it immediately when you need it later.
grep -q '^root:' /etc/passwd
status=$?
printf 'saved grep status=%d\n' "$status"
# This printf itself now becomes the most recent command.
printf 'current $? after printf will no longer be the grep status\n'A common bug is to run a diagnostic command before saving $?. The diagnostic succeeds, and the script accidentally records that status instead of the operation being checked.
3. if executes a command list; it does not require [ ]
The grammar of if is built around command status. The condition can be any command or command list.
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
printf 'inside a Git work tree\n'
else
printf 'not inside a Git work tree\n'
fiThe [ ... ] syntax you often see is simply another command that calculates a status. Bash does not require it around every condition.
4. && and || are status-driven command lists
cmd1 && cmd2 runs cmd2 only if cmd1 succeeds. cmd1 || cmd2 runs cmd2 only if cmd1 fails.
mkdir -p output &&
printf 'directory ready\n'
test -f config.env ||
printf 'config.env is absent\n' >&2A && BRun B only when A succeedsUseful for dependent stepsA || BRun B only when A failsUseful for fallback or explicit error handling! AInvert A's success/failure resultUseful when absence is the expected conditionFor important branching, if blocks usually communicate intent and error handling more clearly.
5. ! negates a command result
The reserved word ! reverses the status used by shell control flow.
if ! command -v jq >/dev/null 2>&1; then
printf 'jq is not installed\n' >&2
fiNegation changes the logical result. If you also need the original status value for diagnosis, structure the code explicitly rather than relying on $? after negation.
6. Non-zero does not always mean the same thing
Many commands use several non-zero statuses to represent different conditions. grep is a classic example: status 0 means a match was found, 1 means no selected line matched, and a larger failure status indicates an error.
if grep -q 'READY' app.log; then
printf 'ready marker found\n'
else
status=$?
case $status in
1) printf 'ready marker not found\n' ;;
*) printf 'grep failed with status %d\n' "$status" >&2 ;;
esac
fiAn expected negative result and an operational failure are not necessarily the same thing. Read the command's documented exit-status contract before reducing every non-zero value to one generic error.
7. Exit-status numbers carry conventions, not universal meanings
Shells and programs commonly use small positive integers for failure categories. Status 126 and 127 have conventional shell meanings related to command execution; statuses derived from signals may also appear in ranges based on shell conventions. Application-specific statuses vary.
bash -c 'exit 42'
printf 'custom status=%d\n' "$?"
bash -c 'command_that_does_not_exist'
printf 'not-found status=%d\n' "$?"Do not invent a global interpretation such as “2 always means configuration error.” Define and document status meanings within your own script or follow the command's documented interface.
8. Functions also return status
A Bash function returns the status of its last command unless you use return N explicitly.
service_exists() {
local name=$1
grep -Fxq "$name" services.txt
}
if service_exists api; then
printf 'api exists\n'
fiThis lets functions become reusable predicates. Prefer predicate functions that are quiet on stdout unless output is part of their documented interface.
9. Validate prerequisites before mutation
One safe DevOps pattern is to perform read-only checks first, then change state only after all mandatory conditions succeed.
require_command() {
local name=$1
if ! command -v "$name" >/dev/null 2>&1; then
printf 'error: required command missing: %s\n' "$name" >&2
return 1
fi
}
require_command git || exit 1
require_command bash || exit 1
printf 'prerequisites satisfied\n'This keeps failures close to their cause and prevents partial work from starting under an invalid environment.
10. Hands-on lab: build a status-aware health check
Create a script that distinguishes a healthy marker, an absent marker, and an unreadable/missing file.
mkdir -p "$HOME/devops-academy/bash/chapter04/lesson01"
cd "$HOME/devops-academy/bash/chapter04/lesson01"
cat > health.sh <<'EOF'
#!/usr/bin/env bash
log_file=${1:-app.log}
if [[ ! -r $log_file ]]; then
printf 'ERROR unreadable log: %s\n' "$log_file" >&2
exit 2
fi
if grep -q 'STATUS=READY' "$log_file"; then
printf 'READY\n'
exit 0
else
status=$?
if (( status == 1 )); then
printf 'NOT_READY\n'
exit 1
fi
printf 'ERROR grep failed with status %d\n' "$status" >&2
exit 3
fi
EOF
printf 'STATUS=READY\n' > app.log
bash health.sh app.log; printf 'status=%d\n' "$?"
printf 'STATUS=STARTING\n' > app.log
bash health.sh app.log; printf 'status=%d\n' "$?"
bash health.sh missing.log; printf 'status=%d\n' "$?"Verification checklist
11. Knowledge check
Question 1. What status value acts as success in Bash control flow?
Question 2. Why should you save $? immediately?
Question 3. Does if require a [ ... ] expression?
Question 4. Why can blindly treating every non-zero grep result as an execution error be wrong?
12. Summary
Bash's Boolean foundation is process status. Commands that return zero succeed; non-zero statuses represent false or failure conditions according to each command's contract. if, &&, ||, and ! all operate on this model. Preserve statuses promptly and distinguish expected negative outcomes from genuine execution errors.
13. Further reading
- GNU Bash Reference Manual — Exit Status and Conditional Constructs.
- GNU Bash Reference Manual — Lists of Commands.
- POSIX Shell Command Language — exit status.
- GNU grep manual — exit-status meanings.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.