# Case Statements

> Source: https://learn-bash.net/case-statements/
> Part of Learn Bash, free to read.

```bash
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:

```bash
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:

```bash
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
```

```bash
case "$path" in
  /*)      echo "absolute" ;;
  ./*|../*) echo "explicitly relative" ;;
  *)       echo "relative" ;;
esac
```

:::warn Quote the word, never the pattern
```bash
case "$input" in     # quoted — stops word splitting on the value
  *.txt)             # NOT quoted — this must stay a glob
```
Quoting a pattern makes it a literal string, so `"*.txt"` matches only the exact three characters `*.txt`. This is the mirror image of the [`=~` quoting rule](/regular-expressions/), and it trips people up in the same way.
:::

## Character classes

```bash
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:

```bash
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:

```bash
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:

```bash
#!/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

```bash
# 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

```bash
#!/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](/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.
