Course outline

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.

About 4 min readContent reviewed
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.

bash Capture a controlled failure before reporting it
operation() { return 7; }
if operation; then
  printf 'unexpected success\n'
else
  status=$?
  printf 'handled=%s\n' "$status"
fi
Expected output
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.

bash Combine finite iteration and mode selection
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
Expected output
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.

Official references