Bash scripting guide · part 14 of 32
The case statement
Matching one value against glob patterns with case, alternatives with |, the default branch, fall-through with ;& and ;;&, and parsing options with case.
case compares one word against a list of patterns and runs the commands of the first pattern
that matches. It replaces long if/elif chains that test the same variable, and its patterns
are globs, the same *, ? and [...] you use for filenames. It is POSIX, so it works in
sh as well as bash, which makes it the portable answer to many [[ == pattern ]] tests.
case WORD in
PATTERN1) commands ;;
PATTERN2 | PATTERN3) commands ;;
*) default commands ;;
esac
Each branch ends with ;;. The word after case is not split or globbed, so it needs no
quotes; most people quote it anyway.
A first example
Pick an action by file extension
for f in README.md data/config.json src/app.py src/main.c photos/beach.png src/Makefile; do
case $f in
*.md) kind="markdown" ;;
*.json | *.yaml) kind="config" ;;
*.py | *.c | *.sh) kind="source code" ;;
*.png | *.jpg) kind="image" ;;
*) kind="unknown" ;;
esac
printf '%-18s %s\n' "$f" "$kind"
done
README.md markdown data/config.json config src/app.py source code src/main.c source code photos/beach.png image src/Makefile unknown
| separates alternative patterns for one branch. * matches anything, so as the last
pattern it is the default branch; src/Makefile fell through to it. Without a *),
a value that matches nothing just skips the whole case.
Order matters
Patterns are tried top to bottom, and only the first match runs. Put specific patterns before general ones, or the general one swallows everything.
A general pattern before a specific one
f=backup.tar.gz
case $f in
*.gz) echo "wrong: treated as plain gzip" ;;
*.tar.gz) echo "tarball" ;;
esac
case $f in
*.tar.gz) echo "right: tarball" ;;
*.gz) echo "plain gzip" ;;
esac
wrong: treated as plain gzip right: tarball
In the first case, *.gz matched before *.tar.gz got a chance. Reordering fixes it.
Character classes and yes/no answers
Bracket expressions match one character from a set, which makes case-insensitive answers easy
without changing any shell option. POSIX classes work inside brackets too, as in [0-9[:upper:]].
Accept yes and no in any case
for answer in y YES Yes no N maybe ""; do
case $answer in
[Yy] | [Yy][Ee][Ss]) result=yes ;;
[Nn] | [Nn][Oo]) result=no ;;
"") result="empty, using default" ;;
*) result="not understood" ;;
esac
echo "'$answer' -> $result"
done
'y' -> yes 'YES' -> yes 'Yes' -> yes 'no' -> no 'N' -> no 'maybe' -> not understood '' -> empty, using default
[Yy][Ee][Ss] matches yes in any capitalization. "" is a pattern that matches only the
empty string. In bash you could also lowercase first with ${answer,,} and match plain
y|yes (Parameter expansion).
Classify input with character classes
for v in 42 -7 3.14 web01 "" "two words"; do
case $v in
"") t="empty" ;;
*[!0-9-]* | ?*-*) t="not an integer" ;;
-*[0-9]* | [0-9]*) t="integer" ;;
*) t="not an integer" ;;
esac
printf '%-10s %s\n' "'$v'" "$t"
done
'42' integer '-7' integer '3.14' not an integer 'web01' not an integer '' empty 'two words' not an integer
*[!0-9-]* matches any value containing a character other than a digit or -, and ?*-*
catches a minus that is not at the start. What remains, with at least one digit, is an
integer. Glob classes are less expressive than regular expressions; for anything more complex
use [[ $v =~ ^-?[0-9]+$ ]].
Quoting inside patterns
Unquoted characters in a pattern are glob syntax; quoted ones are literal. A variable used as a
pattern acts as a glob when unquoted, exactly as on the right side of [[ == ]].
Literal versus glob patterns
for v in '*' 'anything' '?'; do
case $v in
"*") echo "'$v': the literal asterisk" ;;
\?) echo "'$v': the literal question mark" ;;
*) echo "'$v': something else" ;;
esac
done
ext=log
case app.log in *."$ext") echo "matched the extension held in \$ext" ;; esac
'*': the literal asterisk 'anything': something else '?': the literal question mark matched the extension held in $ext
"*" and \? match only those characters. Combining a glob with a quoted variable,
*."$ext", matches any name ending in .log while treating the variable's contents
literally.
Fall-through: ;& and ;;&
Bash adds two terminators besides ;;. ;& falls through and runs the next branch's
commands without testing its pattern, like a C switch without break. ;;& continues
testing the remaining patterns and runs every other branch that also matches.
Run several matching branches
for host in web01 db01 build01; do
tags=()
case $host in
web*|db*) tags+=(production) ;;&
web*) tags+=(public) ;;&
db*) tags+=(backup) ;;&
*) tags+=(monitored) ;;
esac
echo "$host: ${tags[*]}"
done
level=2
case $level in
3) echo "verbose tracing" ;&
2) echo "debug logs" ;&
1) echo "info logs" ;;
esac
web01: production public monitored db01: production backup monitored build01: monitored debug logs info logs
With ;;&, web01 collected tags from every branch that matched, ending with the default.
With ;&, level 2 ran its own branch and then branch 1 without testing it, so higher levels
include everything below them.
Parsing command-line options
The most common real use of case is the loop that walks through a script's arguments. Each
option is a branch; shift moves to the next argument. This handles long options, which the
getopts builtin cannot (Script arguments).
An argument parsing loop
#!/usr/bin/env bash
verbose=0 env=staging files=()
while (( $# > 0 )); do
case $1 in
-v | --verbose) verbose=1 ;;
-e | --env) env=$2; shift ;;
--env=*) env=${1#--env=} ;;
-h | --help) echo "usage: deploy.sh [-v] [-e ENV] FILE..."; exit 0 ;;
--) shift; files+=("$@"); break ;;
-*) echo "deploy.sh: unknown option: $1" >&2; exit 2 ;;
*) files+=("$1") ;;
esac
shift
done
echo "env=$env verbose=$verbose files=${files[*]}"
bash deploy.sh -v --env=prod app.tar.gz
bash deploy.sh -e dev -- -weird-name.txt
bash deploy.sh --force
env=prod verbose=1 files=app.tar.gz env=dev verbose=0 files=-weird-name.txt deploy.sh: unknown option: --force
--env=prod was split with a parameter expansion, -e dev consumed the next argument with
an extra shift, and -- ended option parsing so a filename starting with - got through.
An unknown option exits with status 2, the usual code for usage errors.
Extended patterns
With shopt -s extglob, bash patterns gain @(a|b) (one of), !(pattern) (anything except),
+(...) (one or more), *(...) (zero or more) and ?(...) (zero or one), which work in case
too. The option must be on before bash parses the case, so set it near the top of the
script, not on the same line. (Inside [[ ]], bash enables these patterns automatically.)
See Globbing for the full syntax.
Extended patterns in case
shopt -s extglob
for v in 42 -7 3.14 007 web01 deploy; do
case $v in
?(-)+([0-9])) echo "$v: integer" ;;
!(*[0-9]*)) echo "$v: no digits at all" ;;
*) echo "$v: something else" ;;
esac
done
42: integer -7: integer 3.14: something else 007: integer web01: something else deploy: no digits at all
?(-)+([0-9]) reads as "an optional minus, then one or more digits", which replaces the
two-pattern trick from the character class example. !(*[0-9]*) matches values with no digit
anywhere: deploy qualifies, while web01 and 3.14 fall through to the default.
Related
getoptsParse short command-line options such as -v and -o FILE in a script, one option per call, inside a while loop.shiftDrop the first N positional parameters ($1, $2, ...) and renumber the rest down.testEvaluate a condition about files, strings or integers and return 0 for true or 1 for false.- ConditionalsHow if, elif, &&, || and ! decide using exit statuses, the three test syntaxes, guard clauses, and why
a && b || cis not an if-else. - Test operatorsEvery file, string and number test for [ ] and [[ ]], pattern and regex matching, and the traps like
[ $a > $b ]creating a file. - GlobbingHow *, ? and [...] match file names, what happens when nothing matches, and the shell options nullglob, failglob, dotglob, globstar and extglob.
- Script argumentsPositional parameters $1 to ${10}, $# and $0, why "$@" is almost always right and "$*" rarely, shift, getopts, and ending options with --.
- Loopsfor over lists, files and ranges, C-style for, while and until, break and continue, retry loops, and redirecting a whole loop's input or output.