Case Statements
Pattern matching on a string — cleaner than a chain of if/elif, and the backbone of every argument parser and dispatch table in shell.
case "$1" in
start)
echo "starting"
;;
stop)
echo "stopping"
;;
restart)
echo "restarting"
;;
*)
echo "usage: $0 {start|stop|restart}" >&2
exit 2
;;
esacEach branch ends with ;;. *) is the catch-all and goes last. There is no fallthrough by default, so no break is needed.
Patterns, not strings#
The patterns are globs, which is what makes case more useful than a string comparison:
case "$file" in
*.tar.gz|*.tgz) tar -xzf -- "$file" ;;
*.tar.bz2) tar -xjf -- "$file" ;;
*.zip) unzip -- "$file" ;;
*.gz) gunzip -- "$file" ;;
*) echo "unknown archive: $file" >&2; exit 1 ;;
esac| separates several patterns for one branch. The full glob syntax applies:
case "$answer" in
[Yy]|[Yy][Ee][Ss]) confirmed=1 ;; # y, Y, yes, YES, Yes...
[Nn]|[Nn][Oo]) confirmed=0 ;;
"") confirmed=0 ;; # empty input
*) echo "answer y or n" >&2; exit 2 ;;
esaccase "$path" in
/*) echo "absolute" ;;
./*|../*) echo "explicitly relative" ;;
*) echo "relative" ;;
esacCharacter classes#
case "$value" in
''|*[!0-9]*) echo "not a number" >&2; exit 1 ;;
*) echo "numeric" ;;
esacThat first pattern is a compact idiom worth knowing: empty, or contains any character that is not a digit. [!...] is the shell's negated class — note it is !, not ^ as in regex.
With shopt -s extglob you get more:
shopt -s extglob
case "$name" in
+([0-9])) echo "all digits" ;; # one or more
?(lib)*.so) echo "shared object" ;; # optional
!(*.tmp)) echo "not a temp file" ;; # negation
esacFallthrough#
Two terminators beyond ;;, both rarely needed:
case "$level" in
error) echo "to pager" ;& # fall INTO the next branch unconditionally
warn) echo "to slack" ;;& # test the NEXT pattern too
info) echo "to logfile" ;;
esac;;— stop (the normal case);&— run the next branch's body without testing its pattern;;&— continue testing subsequent patterns
;;& is genuinely useful for "apply every rule that matches", such as a log router. ;& is rare.
The dispatch pattern#
The most common real use — a script with subcommands:
#!/usr/bin/env bash
set -Eeuo pipefail
cmd_build() { echo "building…"; }
cmd_test() { echo "testing…"; }
cmd_deploy() { echo "deploying to ${1:?deploy needs an environment}"; }
usage() {
cat <<'EOF'
usage: run.sh <command> [args]
build compile everything
test run the test suite
deploy <env> deploy to an environment
EOF
}
main() {
local cmd=${1:-}
[[ $# -gt 0 ]] && shift
case "$cmd" in
build) cmd_build "$@" ;;
test) cmd_test "$@" ;;
deploy) cmd_deploy "$@" ;;
''|-h|--help) usage ;;
*) printf 'unknown command: %s\n\n' "$cmd" >&2; usage >&2; exit 2 ;;
esac
}
main "$@"${1:-} guards against an unset $1 under set -u, and ${1:?message} inside cmd_deploy gives a clear error when the environment argument is missing.
case versus if#
# a chain of string comparisons — verbose, and easy to get quoting wrong
if [[ "$f" == *.txt ]]; then
handle_text
elif [[ "$f" == *.md ]]; then
handle_markdown
elif [[ "$f" == *.json ]]; then
handle_json
fi
# the same thing
case "$f" in
*.txt) handle_text ;;
*.md) handle_markdown ;;
*.json) handle_json ;;
esacUse case when you are matching one value against several patterns, and if when the conditions are genuinely different tests (numeric comparisons, file tests, command exit codes).
Exercise#
#!/bin/bash
# Write a `classify` function taking a filename and printing one of:
# "image", "document", "archive", "script", or "unknown"
# based on the extension. Handle: jpg jpeg png gif / pdf doc docx txt md
# / zip tar gz tgz / sh bash py.
# Match case-insensitively for the image extensions.
set -euo pipefail
for f in photo.JPG report.pdf backup.tar.gz deploy.sh mystery.xyz; do
echo -n "$f -> "
# write your code here
echo
doneCommon questions#
Why does my pattern not match?#
Usually because it is quoted. case "$x" in "*.txt") looks for the literal characters *.txt. Quote the word being matched, never the pattern.
Can I match a regular expression in case?#
No — case uses globs. For regex use [[ $x =~ pattern ]], covered in regular expressions. Globs are usually enough for filenames and simple prefixes, and they are easier to get right.
Why [!0-9] rather than [^0-9]?#
! is the shell's negation inside a bracket expression; ^ is the regex spelling. Bash accepts ^ in some contexts for compatibility, but ! is the portable and correct form for globs.
Get the Bash agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Bash. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.