Foundations Updated 2026-09 View as Markdown

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.

shell
case "$1" in
  start)
    echo "starting"
    ;;
  stop)
    echo "stopping"
    ;;
  restart)
    echo "restarting"
    ;;
  *)
    echo "usage: $0 {start|stop|restart}" >&2
    exit 2
    ;;
esac

Each 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:

shell
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:

shell
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 ;;
esac
shell
case "$path" in
  /*)      echo "absolute" ;;
  ./*|../*) echo "explicitly relative" ;;
  *)       echo "relative" ;;
esac

Character classes#

shell
case "$value" in
  ''|*[!0-9]*)  echo "not a number" >&2; exit 1 ;;
  *)            echo "numeric" ;;
esac

That 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:

shell
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
esac

Fallthrough#

Two terminators beyond ;;, both rarely needed:

shell
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:

shell
#!/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#

shell
# 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 ;;
esac

Use 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#

shell
#!/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
done

Common 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.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.