Bash from the ground up

Matching with case

One value, several patterns, and a branch for each: how case replaces a ladder of tests, and why its patterns are not quite the tests you already know.

Part 5 of 12

Intermediate14 min readBack to Courses

A tidier ladder

When one value has to be compared against several possibilities, a chain of if and elif works but reads badly. case puts the value at the top once and lists the possibilities below it, each with the lines to run and a ;; to close the branch. The list ends with esac, which is case written backwards. The shape is worth learning because every later case block uses the same three parts: the value, the patterns, and the closing ;;.

The value being tested is usually quoted — case "$action" in — because a value with a space in it would otherwise be read as several words. The quotes around the value are not part of the pattern; they only keep the value in one piece.

Patterns, not just exact text

Each possibility is a pattern, not only a fixed word. A star stands for any run of characters, so a name that ends in a given suffix can be matched directly, and a single question mark stands for exactly one character. Several patterns can share one branch if you separate them with a vertical bar. A pattern is matched against the whole value, not a substring, which is why a star is usually needed at one end.

The final pattern is usually just a star, which matches everything that fell through. That makes it the "anything else" branch, and it is worth adding deliberately rather than leaving the script silent about values it did not expect. A default branch also documents that the list of possibilities is closed.

A value, and the shapes it might arrive in

case "$action" in
  start)        echo "starting" ;;
  stop)         echo "stopping" ;;
  restart|reload) echo "reloading" ;;
  *)            echo "unknown action: $action" ;;
esac

# The patterns match the whole value, so this catches
# file names by their endings.
case "$file" in
  *.sh)  echo "a script" ;;
  *.txt) echo "a note" ;;
  ?.md)  echo "a one-letter markdown file" ;;
  *)     echo "something else" ;;
esac

First match wins

case works down the list in order and runs the first pattern that matches the whole value. It does not test every branch, and it does not fall through: the ;; ends the branch. Order therefore matters, because a broad pattern placed above a narrow one takes the work the narrow one was written for. The same rule makes the order of the patterns the thing to check when a branch seems to be ignored.

The value is expanded once, when the case statement starts. Quoting it is the habit that keeps a value with spaces in one piece, while the patterns themselves stay unquoted so their stars and question marks can do their job.

The shape of a pattern

Patterns are glob patterns, the same kind used for file names, not regular expressions. * matches any run of characters including none, ? matches exactly one, and [aeiou] matches one character from the set. A vertical bar separates alternatives inside one branch, so start|restart) handles both words with one block. A bracket expression matches one character from the set, and a range such as [a-z] is shorthand for a run of letters.

Because the pattern has to match the whole value, *.sh means "anything ending in .sh" and a* means "anything starting with a". A pattern with no star is an exact comparison, which makes case a readable replacement for a chain of string tests.

Case is a loop over possibilities

It helps to read a case block as a small machine: the value goes in at the top, each pattern is tried in turn, and the first match runs its lines and leaves. The ;; is the exit from that branch. A branch that never matches is invisible, and a branch placed after a broader one is effectively dead code.

The value is expanded once, before the patterns are tried. That is why a value with spaces needs quotes and why a pattern such as *.sh does not: the star is part of the pattern language, and quoting it would turn it into a literal star. Keep the two kinds of quoting separate in your head and most case bugs disappear.

One value at a time

Run it, then change `action` to `start` and to `jump` to see the other branches. Add a branch that matches two actions at once with a vertical bar.

Output
Click "Run" to execute your code

Do not forget the ;; at the end of a branch. Without it the shell keeps reading into the next branch, and two blocks of lines run where you meant one — a mistake that shows up as duplicated output rather than as an error message.

Three things go wrong in case blocks: a missing ;; lets the next branch run too, quoting a pattern such as "*.sh" turns the star into a literal character, and forgetting the final *) leaves unexpected values silent. The default branch is not required by the shell, but it is what turns a wrong input into a message.

Try these next

  • Add a stop|halt branch that prints the same line for both words.
  • Match a three-letter file name with ??? and see which of your test values qualify.
  • Remove the final *) branch, run the script with an unknown value, then put the branch back.

Classify a list of values

Run it, then add two values of your own — one script name and one unknown word — and check which branch each lands in.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

How does a case block end?