if, elif, else, and Compound Conditions
Condition operators are only the raw material. Production scripts also need readable decision structure. The goal is to make the happy path, rejected states, recoverable conditions, and fatal failures obvious to the next engineer who reads the file.
Learning objectives
By the end of this lesson
- Write clear if/elif/else branches around command status.
- Use compound Bash conditions without hiding intent.
- Apply guard clauses to reject invalid state early.
- Separate expected alternatives from errors.
- Refactor deeply nested logic into readable predicates and functions.
1. if controls command lists
A Bash if statement executes the then list when its condition command list returns success. elif adds ordered alternatives, and else provides a final fallback.
if condition_command; then
printf 'first branch\n'
elif another_condition; then
printf 'second branch\n'
else
printf 'fallback branch\n'
fiThe condition can be a test expression, function, external program, pipeline, or compound list. What matters is its status.
2. elif branches are evaluated in order
Only the first successful branch runs. Put specific rules before broad fallbacks.
environment=${1:-}
if [[ $environment == prod ]]; then
printf 'apply production controls\n'
elif [[ $environment == staging ]]; then
printf 'apply staging controls\n'
elif [[ $environment == dev ]]; then
printf 'apply development defaults\n'
else
printf 'unknown environment\n' >&2
exit 2
fi3. Guard clauses keep the main path shallow
Instead of nesting the entire script inside repeated validity checks, reject invalid conditions early.
config=${1:-}
if [[ -z $config ]]; then
printf 'missing config argument\n' >&2
exit 64
fi
if [[ ! -r $config ]]; then
printf 'config is not readable: %s\n' "$config" >&2
exit 2
fi
# The main workflow now begins with prerequisites established.
printf 'processing %s\n' "$config"Guard clauses reduce indentation and make the assumptions of the main workflow explicit.
4. Compound tests express relationships between predicates
Within Bash [[ ... ]], combine related predicates with && and ||.
environment="prod"
replicas=4
tag="2026.08.09"
if [[ $environment == prod && $tag != latest ]] && (( replicas >= 2 )); then
printf 'production deployment passes basic policy\n'
fiThis example mixes a string conditional command and arithmetic command at the shell-list level. You could also keep numeric comparison inside [[ ]] with -ge. Pick one form consistently enough that readers can parse it quickly.
5. Use grouping when precedence is not visually obvious
When a rule mixes “and” and “or,” parentheses or helper predicates make the intended logic explicit.
role="worker"
environment="prod"
enabled="yes"
if [[ ( $role == api || $role == worker ) && $environment == prod && $enabled == yes ]]; then
printf 'production workload enabled\n'
fiDense conditions are difficult to test. When a condition grows beyond a few meaningful clauses, move part of it into a function with a descriptive name.
6. Predicate functions turn policy into vocabulary
Functions that return success/failure can make high-level control flow read like the domain.
is_supported_environment() {
[[ $1 == dev || $1 == staging || $1 == prod ]]
}
is_immutable_tag() {
[[ -n $1 && $1 != latest ]]
}
if is_supported_environment "$environment" &&
is_immutable_tag "$tag"; then
printf 'deployment inputs accepted\n'
fiKeep predicate functions focused. If they print data, mutate global variables, and perform network requests while also serving as conditions, their behavior becomes difficult to reason about.
7. Branch directly on the operation when that is the real test
If you want to know whether an operation succeeds, often the best condition is the operation itself.
if mkdir -p "$workspace"; then
printf 'workspace ready\n'
else
printf 'could not prepare workspace\n' >&2
exit 1
fiThis is generally stronger than checking a proxy condition and then assuming the operation will succeed.
8. Separate recoverable alternatives from fatal errors
A cache miss may be an expected alternative; a corrupt cache database may be a failure. Structure those outcomes differently.
if artifact=$(read_cache "$key"); then
printf 'cache hit\n' >&2
elif fetch_from_origin "$key" > artifact.tmp; then
printf 'cache miss; fetched from origin\n' >&2
artifact=$(cat artifact.tmp)
else
printf 'could not obtain artifact\n' >&2
exit 1
fiReal implementations should define how read_cache distinguishes “not found” from “cache subsystem failed.” Status contracts matter.
9. Short-circuit lists are best for simple actions
Shell-level && and || are concise, but a classic trap is treating A && B || C as a general ternary expression. If B itself fails, C runs even though A succeeded.
# Potentially misleading:
condition && success_action || fallback_action
# Clear:
if condition; then
success_action
else
fallback_action
fiUse if when the success action can fail independently. The longer form preserves the meaning of each status.
10. Refactor deep nesting before it becomes policy spaghetti
Deeply nested conditions combine validation, policy, and action into one visual block. Refactor by separating:
- input validation,
- environment discovery,
- policy predicates,
- the operation,
- post-operation verification.
validate_inputs || exit 64
discover_environment || exit 2
if deployment_allowed; then
perform_deployment || exit 3
verify_deployment || exit 4
else
printf 'deployment rejected by policy\n' >&2
exit 5
fi11. Hands-on lab: write a deployment decision engine
Create a script that validates environment, tag, replica count, and an approval flag, then prints the selected decision.
mkdir -p "$HOME/devops-academy/bash/chapter04/lesson04"
cd "$HOME/devops-academy/bash/chapter04/lesson04"
cat > decision.sh <<'EOF'
#!/usr/bin/env bash
environment=${1:-}
tag=${2:-}
replicas=${3:-}
approved=${4:-no}
if [[ -z $environment || -z $tag || -z $replicas ]]; then
printf 'usage: %s ENV TAG REPLICAS [yes|no]\n' "$0" >&2
exit 64
fi
if [[ ! $replicas =~ ^[0-9]+$ ]]; then
printf 'replicas must be numeric\n' >&2
exit 2
fi
replicas=$((10#$replicas))
if [[ $environment == prod ]]; then
if [[ $tag == latest ]]; then
printf 'REJECT: production requires immutable tag\n'
exit 3
elif (( replicas < 2 )); then
printf 'REJECT: production requires at least 2 replicas\n'
exit 4
elif [[ $approved != yes ]]; then
printf 'REJECT: production approval missing\n'
exit 5
else
printf 'ALLOW: production deployment\n'
fi
elif [[ $environment == staging ]]; then
printf 'ALLOW: staging deployment\n'
elif [[ $environment == dev ]]; then
printf 'ALLOW: development deployment\n'
else
printf 'REJECT: unsupported environment\n'
exit 6
fi
EOF
bash decision.sh prod 2026.08.09 3 yes
bash decision.sh prod latest 3 yes || true
bash decision.sh staging latest 1 noVerification checklist
12. Knowledge check
Question 1. Why are guard clauses useful?
Question 2. Does Bash evaluate every elif branch after one succeeds?
Question 3. Why can A && B || C be misleading as a ternary replacement?
A && B list fails, including when A succeeded but B failed.Question 4. When is branching directly on an operation preferable to pre-checking?
13. Summary
Readable Bash decisions start with status-driven if blocks, ordered alternatives, and explicit failure paths. Guard clauses keep workflows shallow, predicate functions give policy meaningful names, and direct operation checks reduce race-prone assumptions. Prefer clarity over compressed Boolean tricks.
14. Further reading
- GNU Bash Reference Manual — Conditional Constructs and Lists.
- GNU Bash Reference Manual — Shell Functions.
- POSIX Shell Command Language — if conditional construct.
- ShellCheck documentation — common conditional-control-flow pitfalls.
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.