case Patterns and Defensive Branching
Long chains of string comparisons often signal that a script needs a multi-way dispatch construct. Bash `case` matches one word against ordered shell patterns and gives each branch a clear block, making it ideal for environment routing, command dispatch, file handling, and defensive input validation.
Learning objectives
By the end of this lesson
- Write case statements with exact and pattern branches.
- Combine multiple alternatives in one pattern list.
- Understand ;;, ;&, and ;;& termination behavior.
- Use case for subcommand and environment dispatch.
- Design explicit default branches that reject unknown states.
1. case performs ordered pattern dispatch
A case statement expands one word, then compares it against branch patterns in order. The first matching branch runs under normal ;; termination.
environment=${1:-}
case $environment in
dev)
printf 'development\n'
;;
staging)
printf 'staging\n'
;;
prod)
printf 'production\n'
;;
*)
printf 'unsupported environment: %s\n' "$environment" >&2
exit 2
;;
esacflowchart TB
I["input word"] --> D{"first matching case pattern"}
D --> A["branch actions"]
A --> E["esac / continue script"]
D --> X["default * branch if provided"]2. Branches use shell patterns, not regular expressions
case patterns use shell pattern syntax. Wildcards such as * and ? are often enough for routing filenames and identifiers.
file="service-prod.yaml"
case $file in
*.yaml|*.yml)
printf 'YAML configuration\n'
;;
*.json)
printf 'JSON configuration\n'
;;
*)
printf 'unsupported configuration type\n' >&2
;;
esacThe vertical bar inside a pattern list means “any of these patterns.” It is not a data pipeline in this syntactic position.
3. Quote data, not the wildcard you intend to match
Patterns are syntax. Quoting wildcard characters makes them literal.
value="release-2026"
case $value in
release-*)
printf 'release identifier matched\n'
;;
esacIf part of a pattern comes from data, be explicit about whether that variable should contribute pattern metacharacters or literal text. Defensive scripts avoid letting untrusted input unexpectedly become pattern syntax.
4. Combine semantic aliases in one branch
Multiple patterns can route to the same implementation without duplicating code.
answer=${1:-}
case $answer in
y|Y|yes|YES|Yes)
printf 'confirmed\n'
;;
n|N|no|NO|No)
printf 'declined\n'
;;
*)
printf 'expected yes or no\n' >&2
exit 64
;;
esacFor user-facing interfaces, you may instead normalize case first with parameter expansion and keep fewer alternatives.
5. A defensive * branch makes unsupported state explicit
If the accepted input set is closed—such as deployment environments, subcommands, or modes—include a default branch that rejects everything else.
case $mode in
plan|apply|destroy)
;;
*)
printf 'invalid mode: %s\n' "$mode" >&2
exit 64
;;
esacFor destructive or privileged operations, unknown modes should normally be rejected rather than silently mapped to a permissive default.
6. case is a natural CLI subcommand dispatcher
Many shell utilities grow from one action into several subcommands. A top-level case keeps routing obvious.
usage() {
printf 'usage: %s {status|deploy|rollback} ...\n' "$0"
}
command_name=${1:-}
shift || true
case $command_name in
status)
show_status "$@"
;;
deploy)
deploy "$@"
;;
rollback)
rollback "$@"
;;
help|-h|--help|'')
usage
;;
*)
printf 'unknown command: %s\n' "$command_name" >&2
usage >&2
exit 64
;;
esacLater lessons will cover argument parsing in more depth. The key design point here is separation between routing and implementation.
7. Bash offers three branch terminators
;;Stop after the matching branchNormal and most common behavior;&Execute the next branch's commands without testing its patternIntentional fall-through;;&Continue testing later patternsAllows multiple matching branchesThe nonstandard fall-through forms are Bash features and can surprise readers. Use them only when they make the logic clearer than explicit function calls or separate tests.
value="api-prod"
case $value in
api-*)
printf 'api family\n'
;;&
*-prod)
printf 'production suffix\n'
;;
esac8. Extended patterns can increase power—and complexity
When Bash's extglob option is enabled, richer patterns such as @(...), +(...), and !(...) become available in relevant pattern contexts.
shopt -s extglob
case $environment in
@(dev|staging|prod))
printf 'known environment\n'
;;
*)
printf 'unknown environment\n'
;;
esacFor a small closed set, ordinary dev|staging|prod is simpler. Use extended patterns when they materially improve the rule rather than merely showcasing syntax.
9. case can model a small explicit state machine
Operational scripts sometimes need to branch on observed service state. A case statement can make allowed transitions explicit.
case $state in
stopped)
action="start"
;;
running)
action="verify"
;;
degraded)
action="investigate"
;;
failed)
action="recover"
;;
*)
printf 'unknown service state: %s\n' "$state" >&2
exit 2
;;
esac
printf 'next action=%s\n' "$action"Do not hide a complex distributed state machine inside one shell file. But for small local orchestration decisions, explicit states are often clearer than nested Boolean expressions.
10. Pattern dispatch is useful for artifact handlers
A packaging script may need different commands based on file suffix. case keeps those mappings in one place.
archive=${1:-}
case $archive in
*.tar.gz|*.tgz)
printf 'tar+gzip archive\n'
;;
*.tar.xz)
printf 'tar+xz archive\n'
;;
*.zip)
printf 'zip archive\n'
;;
*)
printf 'unsupported archive: %s\n' "$archive" >&2
exit 65
;;
esacA filename suffix is routing metadata, not proof of file format or safety. Validate content where correctness or security requires it.
11. Hands-on lab: build a defensive service command router
Create a mini CLI with four supported subcommands and a closed set of environments. Unknown commands and environments must fail.
mkdir -p "$HOME/devops-academy/bash/chapter04/lesson05"
cd "$HOME/devops-academy/bash/chapter04/lesson05"
cat > svc.sh <<'EOF'
#!/usr/bin/env bash
subcommand=${1:-help}
environment=${2:-dev}
case $environment in
dev|staging|prod)
;;
*)
printf 'invalid environment: %s\n' "$environment" >&2
exit 64
;;
esac
case $subcommand in
status)
printf 'STATUS env=%s\n' "$environment"
;;
plan)
printf 'PLAN env=%s\n' "$environment"
;;
deploy)
if [[ $environment == prod ]]; then
printf 'DEPLOY env=prod requires external approval check\n'
else
printf 'DEPLOY env=%s\n' "$environment"
fi
;;
rollback)
printf 'ROLLBACK env=%s\n' "$environment"
;;
help|-h|--help)
printf 'usage: %s {status|plan|deploy|rollback} {dev|staging|prod}\n' "$0"
;;
*)
printf 'unknown subcommand: %s\n' "$subcommand" >&2
exit 65
;;
esac
EOF
bash svc.sh status dev
bash svc.sh plan staging
bash svc.sh deploy prod
bash svc.sh unknown dev || true
bash svc.sh status qa || trueVerification checklist
12. Knowledge check
Question 1. Does case use regular expressions by default?
Question 2. What does * commonly represent as the final case pattern?
Question 3. What is the normal branch terminator?
;;.Question 4. Why is an explicit default branch important for destructive command routing?
13. Summary
case is Bash's readable multi-way dispatcher. It matches ordered shell patterns, groups aliases naturally, and makes closed input sets easy to validate. Prefer ordinary ;; branches and explicit catch-all rejection for operational scripts; use fall-through and extended patterns only when they make the rule genuinely clearer.
14. Further reading
- GNU Bash Reference Manual — Conditional Constructs:
case. - GNU Bash Reference Manual — Pattern Matching and
extglob. - POSIX Shell Command Language — case conditional construct.
- ShellCheck documentation — pattern and case-statement diagnostics.
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.