When I first started writing Bash scripts, I leaned on long chains of if/elif/else for everything. It worked, but once I had more than three or four conditions, my scripts turned into a wall of nested logic that was painful to read and even more painful to debug. Switching to case statements changed that completely. In this article I’ll walk through everything I’ve learned about using case in Bash, from the absolute basics to advanced pattern matching tricks I use in production scripts today.
What Is a Case Statement?
A case statement in Bash is a way to match a single value against a list of patterns and run different code depending on which pattern matches. It’s conceptually similar to switch statements in languages like C, Java, or JavaScript, but with Bash’s own glob-style pattern matching instead of strict equality checks.
The basic syntax looks like this:
case "$variable" in
pattern1)
# commands
;;
pattern2)
# commands
;;
*)
# default case
;;
esac
Every case block starts with case and ends with esac (which is just case spelled backward — a Bash convention shared with if/fi and done).
A Beginner Example
Let’s say I want a script that greets a user differently depending on the day of the week:
#!/usr/bin/env bash
day=$(date +%A)
case "$day" in
Monday)
echo "Ugh, Monday. Let's get through it."
;;
Friday)
echo "It's Friday! Almost the weekend."
;;
Saturday|Sunday)
echo "Weekend mode activated."
;;
*)
echo "Just another $day."
;;
esac
Running this on a Wednesday gives:
Just another Wednesday.
Notice the Saturday|Sunday) line — the pipe character lets me match multiple patterns against the same block of code, which is one of the reasons I prefer case over long if/elif chains for this kind of logic.
How It Works Internally
case "$variable" inopens the block and tells Bash which value to compare against.- Each pattern is followed by a closing parenthesis, like
Monday). - The commands after the pattern run only if that pattern matches.
;;terminates each block, telling Bash to stop checking further patterns once a match is found (similar to abreakin other languages’ switch statements).*)acts as a catch-all default, matching anything that didn’t match earlier patterns. Bash checks patterns top to bottom and stops at the first match, so ordering matters.esaccloses the entire structure.
I always quote the variable being tested ("$variable") to avoid word-splitting issues if the value contains spaces.
Pattern Matching: Where Case Statements Really Shine
Unlike a simple equality check, case supports glob-style wildcards, which makes it powerful for parsing input, file extensions, or command-line arguments.
#!/usr/bin/env bash
filename="report.tar.gz"
case "$filename" in
*.tar.gz|*.tgz)
echo "This is a gzipped tarball."
;;
*.zip)
echo "This is a zip archive."
;;
*.txt)
echo "This is a plain text file."
;;
*)
echo "Unknown file type."
;;
esac
Here, *.tar.gz uses the * wildcard to match any filename ending in .tar.gz, regardless of what comes before it.
Real-World Use Case: A Command-Line Argument Parser
One of the most common places I use case is parsing arguments in scripts, often combined with a while loop:
#!/usr/bin/env bash
ACTION=""
VERBOSE=false
while [[ $# -gt 0 ]]; do
case "$1" in
start|stop|restart)
ACTION="$1"
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
echo "Usage: $0 [start|stop|restart] [-v]"
exit 0
;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
done
echo "Action: $ACTION, Verbose: $VERBOSE"
Running ./script.sh restart -v outputs:
Action: restart, Verbose: true
This pattern — a while loop combined with case and shift — is the backbone of nearly every serious command-line tool I write in Bash.
Another Real-World Use Case: A Service Control Script
case statements are the classic building block of System V init scripts, and I still use the same pattern for small custom service wrappers:
#!/usr/bin/env bash
SERVICE="myapp"
case "$1" in
start)
echo "Starting $SERVICE..."
systemctl start "$SERVICE"
;;
stop)
echo "Stopping $SERVICE..."
systemctl stop "$SERVICE"
;;
restart)
echo "Restarting $SERVICE..."
systemctl restart "$SERVICE"
;;
status)
systemctl status "$SERVICE"
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
Advanced Pattern Matching Techniques
Bash’s case supports several pattern types worth knowing:
- Wildcards:
*matches any string,?matches a single character. - Character classes:
[0-9]matches any single digit,[a-zA-Z]matches any letter. - Alternation:
pattern1|pattern2matches either pattern. - Fallthrough with
;&(Bash 4+): normally each block stops at;;, but;&tells Bash to continue executing the next block’s commands regardless of whether its pattern matches. - Fallthrough with
;;&(Bash 4+): continues testing subsequent patterns rather than immediately falling through to the next block’s code.
Example using ;&:
case "$1" in
1)
echo "Case 1"
;&
2)
echo "Case 2 (falls through from 1)"
;;
esac
Running with 1 prints both “Case 1” and “Case 2”, because ;& deliberately falls through.
Best Practices I Follow
- Always quote the variable being matched:
case "$var" in. - Always include a
*)default case, even if it just prints an error — silent failures are hard to debug. - Order specific patterns before general ones, since Bash stops at the first match.
- Keep each case block short; if the logic grows large, call a function instead of writing dozens of lines inline.
- Use
|for related patterns instead of duplicating code across multiple blocks.
Security Considerations
case statements themselves are safe by nature since they only compare strings and don’t execute arbitrary code from the pattern. That said:
- Be careful if the matched variable comes from untrusted input and is later used to build a command — pattern matching doesn’t sanitize the value, it only decides which branch runs.
- Avoid using
evalinside case blocks with user-supplied values, as that reintroduces the classic shell injection risk regardless of how the value was chosen.
Optimization Tips
caseis generally faster than an equivalentif/elifchain for many conditions because Bash can short-circuit pattern matching more efficiently in some implementations, though for a handful of conditions the difference is negligible.- For extremely large pattern sets (dozens of options), consider an associative array lookup instead, which can be more maintainable and just as fast.
Troubleshooting
- My pattern with a wildcard isn’t matching: double check you’re not accidentally quoting the pattern itself, like
"*.txt". Quoting a pattern disables glob expansion and turns it into a literal string match. - Fallthrough isn’t working: make sure you’re using
;∨;&and running Bash 4.0 or later; earlier versions don’t support these operators. Check your version withbash --version. - The default case runs even when I expect a match: verify pattern ordering — a broader pattern earlier in the list may be matching before your specific one gets a chance.
Common Mistakes to Avoid
- Forgetting the closing
;;after a block, which causes Bash to keep executing into the next pattern’s commands unintentionally. - Not quoting the tested variable, leading to word-splitting issues with multi-word values.
- Using
casefor numeric range comparisons without character classes —casedoes string/glob matching, not numeric comparison, so[0-9]style classes or a helperifwith-lt/-gtare needed for true numeric ranges.
FAQs
Can case compare numbers directly, like case "$n" in 5) ... ;;? Yes, for exact matches this works fine since it’s really just string comparison, but for ranges you need character classes or a different construct like if [[ "$n" -gt 5 ]].
Is case faster than if/elif? For a small number of conditions, the difference is negligible. For many conditions, case is usually more readable and can be marginally faster.
Can I use regular expressions in case? No, case uses glob patterns, not full regular expressions. For true regex matching, use [[ "$var" =~ pattern ]] instead.
Does case work in sh as well as Bash? Yes, case is part of the POSIX shell specification, so it works in sh, dash, and other POSIX-compliant shells, not just Bash.
Summary
The case statement is one of the most underused tools I see in beginner Bash scripts, yet it’s one of the cleanest ways to handle multi-branch logic, argument parsing, and pattern-based decisions. Once you get comfortable with wildcards, alternation, and fallthrough operators, you’ll find yourself reaching for case instead of long if/elif chains almost every time.
References
- GNU Bash Reference Manual, Conditional Constructs: https://www.gnu.org/software/bash/manual/bash.html#Conditional-Constructs
- POSIX Shell Command Language specification: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
- Bash Hackers Wiki on
case: https://web.archive.org/web/2023/https://wiki.bash-hackers.org/syntax/ccmd/case
