Chapter 04Lesson 04~60 minutes

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.

BeginnerConditionsHands-on lab

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'
fi

The 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
fi

3. 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"
Readability pattern

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'
fi

This 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'
fi

Dense 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'
fi

Keep 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
fi

This 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
fi

Real 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
fi
Avoid pseudo-ternary chains

Use 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
fi

11. 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 no

Verification 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?

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.
Next lesson

case Patterns and Defensive Branching

Continue Chapter 4 by building the next layer of Bash decision logic.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.