Bash / Basic: syntax, values, and control / Lesson 4
Make status and control flow explicit
Use command results, conditions, case selection, and loops without assuming printed output or set -e defines success.
On this page
Lesson goals
- Interpret zero and nonzero statuses using command contracts.
- Choose clear conditions and loop termination.
- Preserve original failure status during diagnostics.
Status is a separate result channel
A command returns a status independently of its standard output and error. Zero conventionally indicates success, while a nonzero value needs the command’s documented interpretation. A search with no matches can be an expected branch rather than a broken program. $? describes the most recent command and is replaced by the next one. Read it immediately or use the command directly as an if condition. Do not infer success from silence, a reassuring message, or the existence of some output; a program can emit partial data before failing.
Test the operation you actually care about
An if condition executes a command list and selects a branch from its status. It need not contain a test utility. Use if operation; then success; else failure; fi when both paths matter. Capture the failed status at the beginning of else before printing anything. A negated condition changes the status visible within its branch, so if ! operation is convenient for branching but does not preserve the original failure in $?. Explicit structure is easier to review than a chain whose middle command can itself fail.
operation() { return 7; }
if operation; then
printf 'unexpected success\n'
else
status=$?
printf 'handled=%s\n' "$status"
fi
handled=7
The synthetic function returns seven, which the else branch captures. The reporting command succeeds, so the complete demonstration ends successfully after handling the expected failure.
Use case for a finite input vocabulary
case matches a value against shell patterns in ordered alternatives. It is useful for a small mode vocabulary, option outcomes, or format selection. A default branch should deliberately reject or handle unknown input. Patterns are not regular expressions, and quoting a pattern can change whether its metacharacters retain pattern meaning. Keep behavior for an empty value explicit. A compact case statement can remove repeated string tests, but it should not hide side effects or merge unrelated failure policies. Prefer one clear result for each allowed mode and a documented error for the rest.
A loop needs a progress and completion contract
for iterates a declared list, while while and until repeatedly test command status. State what changes each iteration and how the loop terminates. break and continue alter flow and can hide required cleanup if placed carelessly. An arithmetic command also returns a status based on its value, so ((count++)) can report nonzero when the old value was zero; that matters in status-sensitive contexts. Do not treat every arithmetic expression as a neutral assignment. Use readable increment placement and test zero, one, empty, and early-failure cases.
total=0
for value in 2 3 4; do
total=$((total + value))
done
case $total in
9) printf 'sum=%s\n' "$total" ;;
*) printf 'unexpected total\n' ;;
esac
sum=9
Only declared synthetic integers enter arithmetic. Assignment avoids using the increment expression’s truth status as the loop’s error policy.
Options cannot replace failure design
set -e has documented exceptions in conditions, lists, pipelines, and function invocation contexts. A function called as a condition can behave differently from the same function called normally. These rules are not a substitute for deciding which operations are required, optional, or expected to fail. Write explicit handling around important work and return a meaningful status to the caller. Intermediate lessons examine errexit and pipelines experimentally. At this stage, make control flow understandable without relying on an invisible global switch, and keep data output separate from explanations of failure.
Common errors and fixes
Reading $? after a diagnostic
- Why it happens
- The diagnostic replaced the original status.
- How to fix it
- Capture immediately on the failure path.
- Prevent it next time
- Review status handling with each message.
Using && and || as a complete if/else
- Why it happens
- A failure in the supposed success action triggers the fallback.
- How to fix it
- Use explicit if when branches have independent failures.
- Prevent it next time
- Draw the actual status flow before compressing syntax.
An increment unexpectedly returns failure
- Why it happens
- Arithmetic truth status was confused with assignment success.
- How to fix it
- Use an appropriate assignment or explicit status handling.
- Prevent it next time
- Test initial zero values in control-sensitive code.
Check your understanding
1. Why can a && b || c run c even when a succeeded?
Show worked solution
If b fails, the left-hand && list has a nonzero result and c runs. This differs from an if whose else depends only on a. Use explicit branching when failure in the success action requires its own handling.
2. Where should an else branch save a required command’s nonzero status?
Show worked solution
As its first status-sensitive action, before printf, logging, or another test. Store status=$? and use the stored value for diagnostics and return. If the condition was negated, redesign it so the original status is still available.
Key takeaways
- Branch on documented statuses and save failures immediately.
- Prefer explicit failure contracts over global assumptions.