Right, so you’ve graduated from a simple if statement. Good for you. But what happens when you have a dozen different potential conditions to check against a single variable? You could write a horrifyingly long chain of if, elif, elif, elif… but you’re better than that. You’re not a monster. Enter the case statement: the Swiss Army knife for pattern matching in bash. It’s cleaner, more efficient, and frankly, it looks a lot more professional.

Think of it as a polite bouncer at a club, checking your variable’s outfit ($variable) against a list of dress codes (patterns). If it matches, it lets your code into the exclusive VIP section to execute. No match? It shows it the door (or a default action, if you’re smart enough to provide one).

Here’s the basic syntax. The double semi-colon ;; is how you say “break out of this case block now,” and the esac (that’s ‘case’ backwards, because why not?) marks the end.

case "$variable" in
  pattern1)
    # commands to execute if pattern1 matches
    ;;
  pattern2)
    # commands to execute if pattern2 matches
    ;;
  *)
    # commands to execute if nothing else matches (the default case)
    ;;
esac

The Power of Patterns (It’s Not Just Strings)

This is where case leaves simple if statements in the dust. The patterns aren’t just literal strings; they’re glob patterns, the same kind you use for filename expansion with * and ?. This is the secret sauce.

read -p "What's your favorite animal? " animal

case "$animal" in
  dog | Dog | DOG)
    echo "Man's best friend. A solid choice."
    ;;
  cat)
    echo "Ah, a superior being who tolerates your presence."
    ;;
  d*)
    echo "Something that starts with 'd'? Duck? Donkey? Dragon?"
    ;;
  *)
    echo "$animal is a weird favorite animal, but you do you."
    ;;
esac

See what we did there? dog | Dog | DOG) uses the | to mean “OR”, matching any of those three. d*) matches anything that starts with a ’d’. The *) at the end is the catch-all default case, which is non-negotiable for writing robust scripts. Always include it. Always. It saves you from silent failures when input is something you didn’t anticipate.

Taming the Wildcards: Quote Your Variables

Here’s your first pitfall, and it’s a classic bash gotcha. You must double-quote your variable expansion in the case line ("$variable"). Why? Because if your variable contains whitespace or glob characters, the shell will perform word splitting and filename expansion before case even gets a look at it. The result will be utter nonsense.

# DANGER: Unquoted variable
user_input="hello * world"
case $user_input in
  # ... patterns will never be checked against "hello * world"
  # because the shell expands the '*' first!
esac

# SAFETY: Quoted variable
user_input="hello * world"
case "$user_input" in
  # Now the pattern is correctly matched against the string "hello * world"
  "hello * world")
    echo "You entered a literal asterisk. How daring."
    ;;
esac

Beyond Simple Globs: Advanced Pattern Matching

The designers didn’t stop at basic globs. They threw in some useful extended patterns, but you have to enable them with shopt -s extglob at the top of your script. This unlocks patterns like ?(pattern) (zero or one occurrence), *(pattern) (zero or more), +(pattern) (one or more), and @(pattern1|pattern2) (one of the given patterns). This is incredibly powerful.

#!/bin/bash
shopt -s extglob  # Enable extended globbing for the pattern matching

read -p "Enter a filename: " filename

case "$filename" in
  # Match files that are .jpg, .jpeg, .png, or .gif
  *.@(jpg|jpeg|png|gif) )
    echo "$filename is an image file."
    ;;

  # Match a file that starts with 'data', followed optionally by a number
  data?([0-9]).txt )
    echo "Found a data file (possibly with a number)."
    ;;

  # Match a file that starts with 'archive' and ends with .tar.gz, .tar.bz2, etc.
  archive*.tar.+(gz|bz2|xz) )
    echo "Found a compressed tarball."
    ;;

  *)
    echo "Unknown file type: $filename"
    ;;
esac

The Fallthrough (And Why Bash Doesn’t Have It)

In languages like C, if you don’t include a break, execution will “fall through” to the next case. Bash is different. The ;; is an explicit break. If you want a fallthrough-like behavior—where one pattern can execute code for multiple matches—you have to be clever and use ;& (if you’re in bash 4.0+) instead of ;;. This tells bash to just keep executing the very next block of commands, regardless of the pattern.

case "$1" in
  start)
    echo "Starting the service..."
    # This will ALSO run the 'status' commands below because we used ;&
    ;&
  status)
    echo "Checking status..."
    systemctl status my_service
    ;;
  *)
    echo "Usage: $0 {start|status}"
    ;;
esac

It’s a weird, niche feature. Use it sparingly, and only with a big comment explaining what you’re doing, or the next person to read your script (probably future you) will be very, very confused. Most of the time, you just want the standard ;; behavior. It’s predictable and safe.