skip to content

In a bash script you write `case "$1" in start|stop) A ;; *.log) B ;; *) C ;; esac`. How does bash compare the word to each pattern, and what happens when more than one pattern could match?

level: juniorimportance: must knowfreq 65%

answer

  1. globs, not regular expressions
  2. the whole word must match
  3. first matching clause wins
  4. | separates alternatives in one clause
  5. *) is just a pattern matching anything

basics

~20 s

Bash compares the word against each pattern using shell glob rules, not regular expressions, and a pattern must match the whole word. Clauses are tested top to bottom and the first match wins; every later clause is skipped.

solid answer

~40 s

A `case` expands the word once, then walks the clauses in written order and compares the word with each pattern using shell pattern (glob) rules: `*` is any string, `?` is any single character, `[...]` is a bracket expression, and everything else is literal. There is no regex here, so `.` is a plain dot and there are no anchors — the pattern has to match the *entire* word, which is why `start` does not match `restart`. Inside one clause, `|` separates alternative patterns. The first clause whose pattern matches runs, and with the usual `;;` terminator bash then jumps past `esac`, so no later clause is even tested. That makes ordering the logic: specific patterns first, and the catch-all `*)` last. `shopt -s nocasematch` makes the comparison case-insensitive.

go deeper

for a junior

Be able to write a correct case block from memory: word, patterns ending in a close paren, commands, then a double semicolon, and esac at the end. Say plainly that the first matching pattern wins.

for a middle

Explain that patterns are globs anchored to the whole word, that | is alternation inside a clause, and show why clause order decides which branch is reachable.

for a senior

Show the review instinct: spot a broad pattern shadowing a specific one, and know that bash silently accepts unreachable clauses so nothing but code review catches it.

for a principal

Own the readability argument — when a dispatcher should be a case, when it has outgrown one, and whether nocasematch or normalising the input is the safer team-wide convention.

## What a case statement actually does `case` is a compound command that takes one word, expands it, and then walks its clauses in the order you wrote them, comparing the expanded word against each clause's pattern. The first pattern that matches wins: bash runs that clause's command list and, with the ordinary `;;` terminator, jumps straight past `esac`. No other pattern is tested and no other clause runs. Almost everything candidates get wrong about `case` follows from missing that one sentence. The shape is: ```bash case "$1" in start|restart) echo "bringing the service up" ;; stop) echo "bringing it down" ;; *.log) echo "that looks like a log file" ;; *) echo "unrecognised: $1" ;; esac ``` Each clause is `pattern[|pattern...]) command-list ;;`. The `|` inside a clause is alternation that belongs to the `case` grammar — it is not a pipeline and it starts no subshell. A leading `(` is also legal (`(start|restart)`), and people use it so that a `case` nested inside `$( )` stays readable to editors that count parentheses. ## Globs, not regular expressions The patterns use shell pattern-matching syntax — the same grammar as filename globbing — not regular expressions. `*` matches any string including the empty one, `?` matches exactly one character, and a bracket expression such as `[0-9]` matches one character from the set. Everything else is literal. So in a `case` pattern: - `.` is a literal dot, not "any character"; - `a*` means "an `a` followed by anything", not "zero or more `a`"; - `^` and `$` are ordinary characters with no anchoring meaning; - `+` and `{2,3}` are literal text. A candidate who reads `[0-9]*` as a regex will tell you it matches a repeated digit; it actually matches any word that *begins* with a digit. If you have enabled `shopt -s extglob`, the extended pattern forms are available in `case` patterns too, but the pattern grammar itself is a topic of its own — for `case` the point is simply that it is a pattern, never a regex. ## The match is anchored at both ends Unlike `grep`, which reports a hit anywhere in the line, a `case` pattern must consume the whole word. `case restart in start) ...` does not match; `case restart in *start*) ...` does. This bites people who port a `grep`-style condition into a dispatcher and quietly lose a branch. ## Order is the control flow Because the first match wins, the order in which you list clauses *is* the program logic. Put the specific patterns above the general ones: a `report.log` clause below a `*.log` clause is dead code, because `*.log` will always be reached first. The conventional last clause is `*)`, which matches anything and therefore acts as the default branch. Writing `*)` first turns every remaining clause into dead code, and bash issues no warning at all — it is a perfectly legal `case`, it just never gets past the first pattern. ## Case-insensitive matching Two idiomatic ways to accept `Y` as well as `y`: ```bash shopt -s nocasematch case $answer in yes|y) echo "confirmed" ;; esac shopt -u nocasematch ``` `nocasematch` is a bash `shopt` that affects `case` (and `[[`) matching, so enable it narrowly and turn it back off. The alternative is to normalise the word before matching — `case ${answer,,} in yes|y)` — using the lowercasing expansion available from bash 4.0 onward. ## Why reach for case at all A chain of string comparisons that all test the *same* word is exactly what `case` is for. One word, many patterns, a visible default, and glob matching built in — no repeated variable name, no repeated test command, and a shape that a reviewer can scan in one pass. That is why the subcommand dispatchers in real init scripts, entrypoints and CLI wrappers are nearly always a `case`.

  • If you have both a `*.log` clause and a `report.log` clause, where must each go and why?
    `report.log` must come first. Clauses are tested top to bottom and the first match wins, so a broader pattern placed above a narrower one makes the narrower clause unreachable. Bash gives no warning about the dead branch, so ordering specific-to-general is a review habit, not something the shell enforces.
  • How would you accept both `y` and `Y` without listing every capitalisation?
    Either enable `shopt -s nocasematch` around the `case` and unset it afterwards, since it makes pattern matching case-insensitive, or normalise the word first with `${answer,,}` to lowercase it before matching. The lowercasing expansion needs bash 4.0 or newer, so on bash 3.2 the `shopt` route (or `tr`) is the portable one.
  • Does a `case` pattern like `*.txt` get expanded against the files in the current directory before matching?
    No. The pattern is used as a pattern, not as a glob to be resolved against the filesystem, so `*.txt` matches the word if the word ends in `.txt` regardless of what files exist. Pathname expansion never happens on the pattern or on the word in a `case`.

saying these in an interview costs you the question

  • Claims case patterns are regular expressions, so dot matches any character
  • Thinks every matching clause runs unless you break out
  • Says matching is substring-based, so start matches restart
  • Puts the *) clause first and wonders why nothing else runs
  • Quotes every pattern by reflex, turning wildcards into literals

context