#

Bash

Subscribe
Bash October 29, 2026·9 min read

Bash Case Statement: Syntax, Pattern Matching, and Examples

Use Bash case when one value needs to choose among several literal strings or shell patterns. Use a test, regex, or option parser when the job is not pattern matching. Start with ;; and verify the intended, unknown, and empty inputs before relying on the script.

Bash uses case to pick commands by comparing a value with a series of patterns. The grammar is short. The trouble starts when a wildcard accepts more than you expected, a quoted pattern turns literal, or a value contains characters you can't see.

Bash case syntax: a working example

bash case syntax

A case statement starts with the value to examine, followed by in. Each clause has a pattern ending in ), some commands, and a terminator such as ;; to end the branch. The closing keyword is esac.

This script accepts start, stop, or restart as its first argument:

#!/usr/bin/env bash

case "${1:-}" in
  start)
    printf 'starting\n'
    ;;
  stop)
    printf 'stopping\n'
    ;;
  restart)
    printf 'restarting\n'
    ;;
  *)
    printf 'usage: %s {start|stop|restart}\n' "$0" >&2
    exit 2
    ;;
esac

Save the code as script.sh and run:

bash script.sh start

Expected output:

starting

The subject "${1:-}" supplies the first positional argument, or an empty value if none was passed. Bash checks patterns in order, selects start), prints starting, and stops at ;;. The *) clause catches everything else.

For bash script.sh unknown, the catch-all writes a usage message to standard error and exits with status 2. That status comes from the script's exit 2 command.

The full grammar is in the GNU Bash Conditional Constructs reference.

Match several values, choose the default, and control branch order

case branch order

Separate alternatives with | when different inputs should do the same thing. A clause like yes|y) accepts either spelling.

case "$answer" in
  yes|y)
    printf 'accepted\n'
    ;;
  no|n)
    printf 'declined\n'
    ;;
  '')
    printf 'no answer\n'
    ;;
  *)
    printf 'unrecognized\n'
    ;;
esac

'') catches an empty answer. The catch-all *) matches an empty string too, so it belongs after the specific patterns. Put it first, and with ;; every later branch becomes unreachable.

A numbered-menu thread on r/bash included this question: “What if the user does not input anything. How do I read that”. With read, an empty response reaches the case statement. Bash's select construct handles a blank line differently: it redisplays the menu before the case body runs.

If no pattern matches and there is no default clause, case returns status 0. To reject an unknown answer, add a clause that catches it and fails.

How Bash case patterns match: wildcards, brackets, and filenames

bash glob patterns

case uses shell glob patterns, and each pattern must match the whole expanded subject. The basic operators are * (any string, including an empty one), ? (one character), and brackets such as [abc] or [0-9] (one character from a set or range).

InputPatternResult
report.txt*.txtMatch
readme.md*.txtNo match
a?Match: one character
abc123abc*Match
photo.jpg*.jpg|*.jpeg|*.pngMatch through the first alternative
5ab[0-9]*Match
digit prefix matching

[0-9]* matches 5ab because the bracket expression takes the first digit and * accepts everything after it:

value=5ab

case "$value" in
  [0-9]*) printf 'prefix_only\n' ;;
  *)      printf 'other\n' ;;
esac

Expected output:

prefix_only

If you need a string made only of digits, test it with [[ $value =~ ^[0-9]+$ ]]. The + means empty input fails.

Bracket ranges follow locale rules, which matters if your script relies on a particular character order.

Operators and matching rules are specified in GNU Bash Pattern Matching.

Quoting patterns and handling case-insensitive matches

quoted bash patterns

Quotes change what a case pattern matches. An unquoted *.txt is a glob for strings ending in .txt. The quoted "*.txt" matches the literal characters *.txt, asterisk included.

It matters most when the pattern comes from a variable:

pattern='*.txt'
file='report.txt'

case "$file" in
  "$pattern")
    printf 'literal\n'
    ;;
  *.txt)
    printf 'glob\n'
    ;;
esac

Expected output:

glob

The first clause compares report.txt with the literal text *.txt and fails. The second clause uses a wildcard and matches. Quote a pattern that comes from a variable when it should match literally.

ShellCheck's SC2254 rule shows the problem with a filename containing [1813]: left unquoted in a pattern expansion, brackets are read as matching syntax.

shell subject expansion

The subject follows different expansion rules. In case $value in, Bash does not split words or expand pathnames in the expanded subject, so a value such as two words stays one value.

For case-insensitive matching, list the spellings: yes|YES|Yes). Bash also has nocasematch, enabled with shopt -s nocasematch. It stays on for later matches too, so restore the previous setting when you're done.

;; vs ;& vs ;;&: when does matching continue?

bash clause terminators

A clause terminator decides whether matching stops, the next commands run, or later patterns get tested.

TerminatorNext actionUse
;;Finish the case after this clausePlain first-match selection
;&Run the next clause's commands without testing that patternCommand continuation
;;&Resume testing subsequent patternsAllow later matching clauses to run

Scripts run by an unspecified /bin/sh or an older shell need their operator syntax checked separately, so don't assume ;& and ;;& are portable.

With ;;, Bash finishes the selected branch. You don't need a C-style break.

End a clause with ;& to continue into the next clause's commands:

case admin in
  admin)
    printf 'first\n'
    ;&
  guest)
    printf 'next\n'
    ;;
esac

Output:

first
next

Bash skips the guest) test, and its commands run because the previous clause ended with ;&.

continued pattern testing

With ;;&, Bash keeps testing the patterns that follow, so overlapping matches can run more than one command list:

role=admin

case "$role" in
  admin)
    printf 'admin\n'
    ;;&
  admin|editor)
    printf 'editor\n'
    ;;&
  *)
    printf 'default\n'
    ;;
esac

Output:

admin
editor
default

The first two patterns match admin. Testing continues to *, which matches too. With ;;&, a trailing catch-all can run after earlier matches.

Use case for one command-line argument, not an entire option parser

bash command arguments

case "$1" in looks at one positional argument only. That works for a program that takes exactly one command, such as start, stop, or restart. Test a command with no arguments, an option followed by a value, and a filename that begins with a hyphen.

A command line with two flags is a different job:

script.sh --archive --info

A single comparison with $1 sees only --archive. To process --info too, the script has to loop over the arguments. Bash's getopts handles short options. Long options call for a separate parser.

When case is better than if, a regex, or select

bash conditional selection

case fits when you compare one value with several literal strings or glob patterns.

TaskConstructWhy it fits
Choose commands or match filename patternscaseOne subject, multiple shell patterns
Evaluate separate Boolean conditionsif / elifIndependent conditional tests
Compare numeric magnitudes(( ... ))Arithmetic comparison
Evaluate a regular expression[[ $value =~ regex ]]Regex matching
Present a numbered interactive menuselectMenu display and choice collection
Parse repeated short optionsgetopts in a loopOption processing across arguments
conditional task comparison

Fix case syntax errors before changing the patterns

bash syntax errors

An error near a closing ) can start with an unfinished clause earlier in the script. Bash reports the point where it can no longer parse the input, which can be the next pattern line.

One r/bash poster building a game in Bash reported, “i have used esac function to close the case but its not working”. Bash raised an unexpected-token error near ) in that script.

esac closes the case construct. It does not run as a function. In a multi-clause statement, a missing terminator can leave the next pattern in the wrong position.

This code is malformed because the first clause has no terminating operator:

case "$choice" in
  1)
    printf 'one\n'
  2)
    printf 'two\n'
    ;;
esac

The fix ends the first clause before the second begins:

case "$choice" in
  1)
    printf 'one\n'
    ;;
  2)
    printf 'two\n'
    ;;
esac
bash parser checking

Check the pattern's closing ), the command list, the clause terminator, and the final esac. If the unexpected token shows up on a later line, look at the preceding clause too.

Once the grammar is fixed, run:

bash -n script.sh

That checks Bash parsing without running the script's commands. A clean result means the syntax is valid, but you still need to test the branch it picks and the output.

Find out why a case pattern does not match

bash pattern debugging

When the clause you expected doesn't run, inspect the string that reached case. Extra bytes can make a value that looks correct fail an exact match.

In an r/bash discussion about AWS EC2 state output, the poster wrote, “Can't really see why this should be the case after I have removed all of the extraneous characters.” The value looked like 16, yet a literal 16) branch failed while *16) matched. A later reply reported a hidden byte.

A leading * admits any prefix before 16.

Bash's printf can show a shell-escaped version of the variable's actual value:

printf '%q\n' "$inst_code"

If you need the bytes and xxd is installed, inspect the same value with:

printf '%s' "$inst_code" | xxd
shell input inspection

Compare the output with the literal value the clause should match. An external command, parser, or transformation can add extra content. Once you know which character is the problem, strip that one.

If the subject is correct, look for an earlier clause that matched first, and check the case-sensitivity and literal-pattern rules.

Verify your case statement with a small test matrix

bash test matrix

Run the script with representative inputs and compare its output and exit status with what you intended.

Invocation or fixtureExpected matchExpected result
bash script.sh startstart)starting. Status 0
bash script.sh restartrestart)restarting. Status 0
bash script.sh unknown*)Usage on stderr. Status 2
bash script.sh*) after empty subjectUsage on stderr. Status 2
bash script.sh STOP*) with default case sensitivityUsage on stderr. Status 2
5ab in the numeric-prefix fixture[0-9]*)prefix_only
admin in the overlapping-pattern fixtureThree matching clauses with ;;&admin, editor, default

For an unknown argument, check both the usage message and the exit status:

bash script.sh unknown
printf 'exit status: %s\n' "$?"

The output has the usage message, then:

exit status: 2
case regression checks

When your script relies on wildcard boundaries or continued pattern testing, include a value that exposes that behavior.

The pattern and continuation behaviors above were reproduced on GNU Bash 5.2.37 on Linux.

bash branching choice
NoScript Team