Bash case Statement (Switch Case)

Tech reviewed: Deepak Prasad
Bash case Statement (Switch Case)

The Bash case statement is the shell’s switch case: you compare one value against several patterns and run the first matching block. Searches like switch case bash shell, bash shell switch case, and bash shell script switch case usually mean this construct—not a separate switch keyword (Bash does not have one).

Think of case as a cleaner alternative to a long if … elif … elif chain when one variable (often $1) must equal one of several known strings. Bash walks the patterns top to bottom, runs the first match, then stops—unless you use the rare ;& / ;;& terminators in Bash 4+.

Use case for script verbs (start / stop), menus, file extensions, and yes/no prompts. For numeric ranges or [[ -f file ]], use Bash if else instead. This page is part of the Shell Scripting & Bash course.

Tested on: Bash 5.2.37; Ubuntu 25.04; kernel 6.14.0-37-generic.


Quick answer: switch case in Bash

bash
case "$1" in
  start)
    echo "Starting..."
    ;;
  stop)
    echo "Stopping..."
    ;;
  *)
    echo "Usage: $0 {start|stop}" >&2
    exit 1
    ;;
esac

Save as demo.sh, then:

bash
chmod +x demo.sh
./demo.sh start
./demo.sh pause

Output:

text
Starting...
Usage: ./demo.sh {start|stop}

How to read the syntax:

  • case opens the statement; in introduces the pattern list.
  • Each pattern) line is one branch (like one case label in C).
  • ;; ends the branch and stops further matching.
  • esac closes the block (case spelled backward).
  • *) is the default branch when nothing else matched.

How pattern matching works

When Bash reaches case WORD in, it compares WORD against each pattern using glob rules (the same family of rules as *.txt in filenames), not regular expressions.

  1. Bash tests the first pattern. If it matches, it runs that clause’s commands until ;;.
  2. If it does not match, Bash tries the next pattern.
  3. When a clause runs, matching stops (with normal ;;) even if later patterns would also match.
  4. If no pattern matches and you have *), the default clause runs.
  5. If nothing matches and there is no *), the case block does nothing and returns exit status 0.

Always quote the word you match ("$1", "$filename"). Unquoted $1 can word-split or glob-expand before case sees it.


Bash case statement syntax

bash
case WORD in
  PATTERN1)
    COMMANDS
    ;;
  PATTERN2|PATTERN3)
    COMMANDS
    ;;
  *)
    DEFAULT_COMMANDS
    ;;
esac
Part Role
WORD Value to match (often "$1" or a variable)
PATTERN Glob-style pattern (not a regex)
|) Separates multiple patterns in one clause (OR)
) Ends the pattern list for this clause
;; End clause; stop matching (default behavior)
*) Default when nothing else matched
esac Closes the case

The opening ( before a pattern is optional in Bash—you will see both start) and (start) in scripts. Official grammar: GNU Bash Conditional Constructs — case.


Basic bash case example

This mimics a tiny service control script—the pattern behind many bash shell switch case for user arguments searches:

bash
#!/usr/bin/env bash

action="${1:-}"

case "$action" in
  start)
    echo "Starting the service..."
    ;;
  stop)
    echo "Stopping the service..."
    ;;
  restart)
    echo "Restarting the service..."
    ;;
  status)
    echo "Service is running."
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|status}" >&2
    exit 1
    ;;
esac

${1:-} means “use $1, or empty string if $1 is unset.” That lets the *) default handle a missing argument cleanly, including when you use set -u.

Run:

bash
chmod +x service.sh
./service.sh start
./service.sh stop
./service.sh restart
./service.sh status
./service.sh pause
./service.sh

Output:

text
Starting the service...
Stopping the service...
Restarting the service...
Service is running.
Usage: ./service.sh {start|stop|restart|status}
Usage: ./service.sh {start|stop|restart|status}

Check exit status after invalid input:

bash
./service.sh pause
echo "exit=$?"

Output:

text
Usage: ./service.sh {start|stop|restart|status}
exit=1

The usage line goes to stderr (>&2) so you can still pipe valid stdout from status without mixing help text into the data stream.


case vs if elif: when to use which

Situation Prefer
Match $1 to start, stop, help case
Compare a number range (-lt, -gt) if / [[ — see compare numbers
Test files (-f, -d) if
Many string literals with | patterns case (clearer than long elif)
Combine unrelated tests with && / || if

Same logic with if (harder to scan):

bash
if [[ "$1" == "start" ]]; then
  echo "Starting..."
elif [[ "$1" == "stop" ]]; then
  echo "Stopping..."
else
  echo "Usage: $0 {start|stop}" >&2
  exit 1
fi

Same logic with case (usual style for verbs):

bash
case "$1" in
  start) echo "Starting...";;
  stop)  echo "Stopping...";;
  *)     echo "Usage: $0 {start|stop}" >&2; exit 1;;
esac

case is readability sugar for “one word, many discrete choices.” It is not meaningfully faster on normal scripts.


case vs C switch

Bash case feels like C switch, but behavior differs:

Feature C switch Bash case
Keyword switch case
Match type Often integer constants Glob patterns on strings
Fall-through Yes unless break No unless ;& / ;;& (Bash 4+)
Default default: *)

In C you must break or execution falls into the next case. In Bash, ;; already stops—you do not add break.


Multiple patterns in one clause

Use | inside a clause when several inputs should run the same commands:

bash
#!/usr/bin/env bash

answer="${1:-}"

case "$answer" in
  y|Y|yes|YES|Yes)
    echo "Confirmed."
    ;;
  n|N|no|NO|No)
    echo "Cancelled."
    ;;
  *)
    echo "Please answer yes or no." >&2
    exit 1
    ;;
esac

Run:

bash
./yesno.sh yes
./yesno.sh NO
./yesno.sh maybe
echo "exit=$?"

Output:

text
Confirmed.
Cancelled.
Please answer yes or no.
exit=1

Without nocasematch, you list variants explicitly (y|Y|yes|…). That is verbose but predictable. For “any capitalization of yes,” see the next section.


Case-insensitive matching

By default, pattern yes does not match the value YES—matching is case-sensitive.

Without nocasematch:

bash
case "YES" in
  yes) echo "matched";;
  *)   echo "no match";;
esac

Output:

text
no match

With nocasematch:

bash
shopt -s nocasematch

case "YES" in
  yes)
    echo "yes (any case)"
    ;;
esac

shopt -u nocasematch

Output:

text
yes (any case)

Enable nocasematch only around the case you need, then turn it off. Leaving it on globally can surprise other scripts or sourced files that expect case-sensitive globs.


Glob patterns in case

Patterns are pathname expansion (globs), not regular expressions. The * in a pattern means “any characters,” similar to *.log in a filename.

bash
#!/usr/bin/env bash

filename="${1:-}"

case "$filename" in
  *.tar.gz|*.tgz)
    echo "Extract with tar -xzf"
    ;;
  *.zip)
    echo "Extract with unzip"
    ;;
  *.log)
    echo "Tail or archive log file"
    ;;
  *)
    echo "Unknown extension: $filename"
    ;;
esac

Run:

bash
./ext.sh backup.tar.gz
./ext.sh archive.zip
./ext.sh app.log
./ext.sh readme.txt

Output:

text
Extract with tar -xzf
Extract with unzip
Tail or archive log file
Unknown extension: readme.txt
Pattern Matches
* Any string (including empty)
? Exactly one character
[abc] One character from the set
start|stop Literal start or stop

For regex matching, use [[ $var =~ pattern ]] inside if, not case.


Bash shell switch case for user arguments

Parsing user arguments is the most common production use. A solid pattern:

  1. Read the verb from $1 (quote it).
  2. List each allowed verb as a pattern.
  3. Put -h|--help in its own clause.
  4. Use *) for unknown verbs, print usage, exit 1.
  5. Validate $2 when a subcommand needs a name (${2:?message}).
bash
#!/usr/bin/env bash
set -euo pipefail

case "${1:-}" in
  -h|--help|help)
    echo "Usage: $0 {create|delete|list} [name]"
    ;;
  create)
    name="${2:?name required}"
    echo "Creating $name"
    ;;
  delete)
    name="${2:?name required}"
    echo "Deleting $name"
    ;;
  list)
    echo "Listing items..."
    ;;
  *)
    echo "Unknown command: ${1:-}" >&2
    echo "Try: $0 --help" >&2
    exit 1
    ;;
esac

Run:

bash
./cli.sh --help
./cli.sh create myapp
./cli.sh list
./cli.sh drop
./cli.sh create

Output:

text
Usage: ./cli.sh {create|delete|list} [name]
Creating myapp
Listing items...
Unknown command: drop
Try: ./cli.sh --help
./cli.sh: line 5: 2: name required

Exit codes:

bash
./cli.sh drop; echo "exit=$?"
./cli.sh create; echo "exit=$?"

Output:

text
Unknown command: drop
Try: ./cli.sh --help
exit=1
./cli.sh: line 5: 2: name required
exit=1

${2:?name required} fails fast when create or delete is missing the second argument—better than silently creating an empty name.

For positional-parameter basics, see script arguments in Bash. For -f / -v flags, pair case with bash getopts.


case inside a getopts loop

getopts parses short options (-f file, -v). Each call returns one option letter in opt; case dispatches what to do for that letter.

bash
#!/usr/bin/env bash

verbose=0
file=""

while getopts "f:v" opt; do
  case "$opt" in
    f)
      file="$OPTARG"
      ;;
    v)
      verbose=1
      ;;
    ?)
      echo "Invalid option" >&2
      exit 1
      ;;
  esac
done
shift $((OPTIND - 1))

echo "file=${file:-none} verbose=$verbose remaining=$*"
  • f) stores the path in OPTARG.
  • v) sets a flag.
  • ?) handles unknown options (getopts sets opt to ?).

Successful run:

bash
./getopts-case.sh -f data.txt -v extra

Output:

text
file=data.txt verbose=1 remaining=extra

Invalid option:

bash
./getopts-case.sh -x
echo "exit=$?"

Output:

text
./getopts-case.sh: illegal option -- x
Invalid option
exit=1

After the loop, shift $((OPTIND - 1)) removes parsed flags so $* holds positional arguments (extra in the example).


Matching numbers (as strings)

case compares text. Digits work because patterns are string literals:

bash
#!/usr/bin/env bash

choice="${1:-}"

case "$choice" in
  1|2|3)
    echo "Small choice"
    ;;
  4|5|6)
    echo "Medium choice"
    ;;
  *)
    echo "Other"
    ;;
esac

Run:

bash
./num.sh 2
./num.sh 5
./num.sh 9

Output:

text
Small choice
Medium choice
Other

This is not math: pattern 10 does not mean “ten” as a number unless the string is exactly 10. For ranges ($n -lt 10), use if [[ … ]] with compare numbers.


Fall-through: ;& and ;;& (Bash 4+)

Most scripts use only ;;. Bash 4.0 added optional fall-through for shared logic:

Terminator Behavior
;; Stop after this clause (default)
;& Run this clause, then run the next clause without testing its pattern
;;& Run this clause, then continue testing later patterns

;;& example (input a matches first clause, then default also runs):

bash
case "$1" in
  a)
    echo "clause a"
    ;;&
  b)
    echo "clause b"
    ;;
  *)
    echo "default"
    ;;
esac

Run ./fall.sh a:

text
clause a
default

Pattern b did not run because ;;& only continues testingb does not match a. The *) default still matches.

Use fall-through sparingly. If two branches share code, a shared function or one clause with | patterns is usually clearer.


case in functions and menus

Interactive menus combine read with case:

bash
show_menu() {
  echo "1) Backup  2) Restore  3) Quit"
  read -r -p "Choice: " choice
  case "$choice" in
    1) echo "Backing up...";;
    2) echo "Restoring...";;
    3) return 0;;
    *) echo "Invalid choice" >&2; return 1;;
  esac
}

Simulate user input without a TTY:

bash
choice=2
case "$choice" in
  1) echo "Backing up...";;
  2) echo "Restoring...";;
  3) echo "Quit";;
  *) echo "Invalid choice";;
esac

Output:

text
Restoring...

See Bash function for return and local variables in larger menus.


Exit status and empty matches

Rules from the Bash manual:

  • No match and no *) clause → case returns 0 (success).
  • A clause ran → exit status is from the last command in that clause.
  • Use *) + exit 1 when invalid input should fail scripts, systemd units, or CI.
bash
#!/usr/bin/env bash

case "$1" in
  ok) true;;
esac
echo "status=$?"

Run three times:

bash
./status.sh
./status.sh ok
./status.sh bad

Output (all three):

text
status=0
status=0
status=0

No argument and bad both fail to match ok, so the body is empty and status stays 0. That surprises newcomers—add *) when “no match” should be an error.


Common mistakes with bash case

  • Forgetting ;; — Bash may try to parse the next pattern as a command, causing syntax errors or accidental execution.
  • Typing switch / esaac — Bash only understands case / esac.
  • Unquoted $1"$1" prevents word-splitting; case $1 in can break on spaces.
  • Expecting regex*.log is a glob; ^foo is not PCRE in case.
  • Using case for file tests — use if [[ -f "$path" ]].
  • Leaving nocasematch on globally — turn it off after the block you need.
  • Assuming no match is an error — without *), unknown input silently does nothing with exit 0.
  • Long elif chains for simple verbs — case is easier to read and diff in code review.

Summary

The bash case statement is Bash’s switch case: case WORD in pattern) commands;; esac. Use it to match script arguments, menu choices, and fixed strings with | patterns and a *) default. Quote the word you match, add sample-level error handling with exit 1, use shopt nocasematch when case-insensitivity matters, and combine getopts + case for flags. Prefer if else for file tests and numeric ranges.


References


Frequently Asked Questions

1. Does Bash have a switch statement?

Bash uses the case keyword, not switch. case word in pattern) commands;; esac is the shell equivalent of a switch statement in C or JavaScript.

2. What is the difference between case and if elif in Bash?

Use case when one value is compared against many fixed patterns. Use if elif when you need numeric ranges, file tests, or compound logical conditions with && and ||.

3. How do you use case with command-line arguments in Bash?

Match $1 or "${1:-}" in a case statement, list each allowed verb as a pattern, and use *) for usage text and a non-zero exit when the argument is invalid.

4. Is Bash case matching case-sensitive?

Yes by default. Run shopt -s nocasematch before case to match patterns without regard to letter case, then shopt -u nocasematch afterward if needed.

5. Does Bash case fall through like C switch?

No by default. The ;; terminator stops after the first match. Bash 4+ adds ;& and ;;& for controlled fall-through when you need shared or continued matching.

6. What exit status does case return?

If no pattern matches and there is no default clause, case returns 0. Otherwise the exit status is that of the last command executed in the matching clause.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with over a decade of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, …

  • Red Hat Certified System Administrator in Red Hat OpenStack
  • Certified Kubernetes Application Developer (CKAD)
  • Red Hat Certified Specialist in Ansible Automation
  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security