A case statement routes one value through an ordered list of shell patterns. It is useful for a command dispatcher, a small set of accepted answers, or filename-suffix classification. Each branch states the values it handles, and several patterns can share one body. The ordering and the default branch are part of the interface: they decide what happens when an input overlaps several categories or belongs to none.
Patterns match the whole value
Case uses shell patterns, not regular expressions. A star matches any string, a question mark matches one character, and brackets specify a character set or range. Thus [0-9]* means a digit followed by anything; it does not mean an entire nonempty string of digits. To validate a decimal identifier without arithmetic interpretation, first reject an empty string and any string containing a non-digit.
The alternatives in start|up|run) share an action. A broad suffix pattern such as *.log) can classify a string without opening a file or searching the directory. Unlike pathname expansion, this matching does not establish that the named resource exists. A slash or leading dot also does not have the special exclusion role it has during ordinary filename expansion. Classification and filesystem validation remain separate operations.
The word being tested undergoes the documented expansions, but Bash does not word-split or glob it as ordinary command arguments. An empty unquoted variable in that position does not delete the syntactic word and cause a case syntax error. Quoting the tested value is still clear style. With nounset enabled, separately handle a missing positional parameter or validate the argument count; quotation alone does not make an unset parameter available.
Once a branch does run, case returns the status of its last executed command. A match is therefore distinct from a successful action. The fixture also checks a selected arm that exits six, ensuring the dispatcher does not turn action failure into success merely because routing succeeded.
When it breaks
With the usual double-semicolon terminator, the first matching branch runs and case finishes. Put exact values before broader families and the catch-all star last. Otherwise a generic option branch can swallow a later exact status command. Syntax checking cannot establish that your ordering expresses the intended precedence; exercise an input matching both patterns and assert which action runs.
If no branch matches, case returns zero without running an action. A missing default is therefore not automatically an error. A dispatcher that rejects unknown commands must explicitly report and return failure in its default branch. Conversely, an optional classification may deliberately allow no match. Decide that policy from the caller’s contract, rather than assuming case supplies input validation on its own.
Quoting a pattern changes its meaning. A quoted "*.log" matches the literal characters, whereas the unquoted pattern matches a suffix family. The same distinction matters when a variable supplies a pattern: decide whether it is a trusted matching expression or literal text. Do not accidentally grant wildcard behavior to an identifier that should be compared exactly.
Bash options and versions introduce further boundaries. Nocasematch changes case sensitivity, while extended patterns need extglob enabled before the relevant code is parsed. Newer Bash also provides branch terminators that continue execution or resume matching; do not assume they behave like double semicolons or work in older shells. The experiment below deliberately uses ordinary patterns and double semicolons compatible with Bash 3.2.
Check it quickly
Run this complete Bash block. Its dispatcher includes exact commands, grouped aliases, a wildcard family, an explicit empty-input error, and an unknown-input error. A second function checks decimal text without accepting a digit followed by letters. The assertions test results and error codes rather than merely looking for a successful script exit.
bash --noprofile --norc <<'BASH'
set -e
export LC_ALL=C
shopt -u nocasematch
route() {
case "${1-}" in
status|--status) printf '%s' status ;;
start|up|run) printf '%s' start ;;
'') return 2 ;;
--*) return 3 ;;
*.log) printf '%s' log ;;
*) return 4 ;;
esac
}
[[ $(route --status) == status ]]
[[ $(route up) == start ]]
[[ $(route 'report final.log') == log ]]
expect_error() {
local expected=$1
shift
if route "$@"; then
return 1
else
local actual=$?
[[ $actual -eq $expected ]]
fi
}
expect_error 2 ''
expect_error 2
expect_error 3 --unknown
expect_error 4 STATUS
digits() {
case "${1-}" in
''|*[!0-9]*) return 1 ;;
*) return 0 ;;
esac
}
digits 007
if digits 7abc; then exit 1; fi
if digits ''; then exit 1; fi
empty=''
matched=no
case $empty in '') matched=yes ;; esac
[[ $matched == yes ]]
case unmatched in known) exit 1 ;; esac
[[ $? -eq 0 ]]
if case match in match) (exit 6) ;; esac; then
exit 1
else
[[ $? -eq 6 ]]
fi
printf 'PASS: precedence, aliases, empty input, validation, no match\n'
BASH
Expect the PASS line with no earlier output. The exact status option must bypass the generic option error, and uppercase STATUS must be rejected because case-insensitive matching is disabled. The final checks demonstrate that an empty expanded word is valid and that no match returns success. These tests cover routing decisions only; replace the harmless actions with real operations only after testing their arguments, side effects, and failure propagation separately.
Sources
- GNU Bash: Conditional Constructs, case ordering, expansion, and return status, checked 2026-09-06.
- GNU Bash: Pattern Matching, wildcard and extended-pattern semantics, checked 2026-09-06.
- GNU Bash: Word Splitting, contexts that split expanded values, checked 2026-09-06.