Case Statements
When a script needs to do something different depending on a single value — like a command name, a menu choice, or an argument the user typed — you could chain a long line of if/elif/else tests. But Bash has a cleaner tool built exactly for this job: the case statement (sometimes called a “switch” in other languages). It matches one value against a list of patterns and runs the block for the first one that matches. This makes scripts that handle commands like start, stop, and restart far easier to read and maintain.
What a case statement is
A case statement takes a value (usually a variable) and compares it, top to bottom, against a series of patterns. The moment it finds a pattern that matches, it runs that block and stops. If nothing matches, an optional catch-all pattern (*) handles the leftover cases.
The word case opens the block and the word esac (that is case spelled backwards) closes it. Here is the basic shape:
case "$variable" in
pattern1)
# commands for pattern1
;;
pattern2)
# commands for pattern2
;;
*)
# fallback for anything else
;;
esac
Two things to notice. Each pattern ends with a single closing parenthesis ). Each block ends with a double semicolon ;;, which tells Bash “this case is done, jump to esac”. Forgetting the ;; is the most common beginner mistake.
Always quote the value you test, like
case "$1" in. If the variable is empty or contains spaces, an unquoted value can cause a syntax error or match the wrong pattern.
When to use case (and when not to)
Reach for case when you are branching on one value against several known options. It shines for service-style scripts, command dispatchers, and menus.
Stick with if when your decision depends on a comparison (if [ "$count" -gt 10 ]), combines several conditions with &&/||, or tests completely unrelated things. case only does pattern matching against a single value — it cannot do numeric “greater than” tests.
| Situation | Use case | Use if |
|---|---|---|
| Match one value to many options | Yes | Awkward |
Numeric range (> 10, < 0) | No | Yes |
| Multiple unrelated conditions | No | Yes |
| Wildcard / pattern matching | Yes | Possible but verbose |
Patterns and wildcards
The real power of case is that patterns are glob patterns — the same wildcards the shell uses for filenames, not regular expressions. The most useful ones:
| Pattern | Matches |
|---|---|
start | The exact word start |
* | Anything (use as the final catch-all) |
*.log | Any value ending in .log |
y|yes|Y|YES | Any one of these alternatives (| is OR) |
[0-9] | A single digit |
[Yy]* | Anything starting with Y or y |
? | Exactly one character of any kind |
You combine alternatives with the pipe symbol |. For example, start|begin) runs the same block whether the user typed start or begin. This is the feature that makes case so much shorter than a stack of if tests.
read -rp "Continue? (y/n) " answer
case "$answer" in
[Yy]|[Yy][Ee][Ss])
echo "Proceeding..."
;;
[Nn]|[Nn][Oo])
echo "Cancelled."
;;
*)
echo "Please answer yes or no."
;;
esac
This accepts y, Y, yes, YES, Yes, and so on, all from two compact patterns.
A real service control script
Here is the classic use case: a script that controls a service and accepts start, stop, restart, or status as its first argument. On Ubuntu (22.04 / 24.04 LTS) the real service manager is systemd, driven by the systemctl command. We will wrap it so the script reads like a friendly menu.
Save this as webserver-ctl.sh:
#!/usr/bin/env bash
# Control the nginx web server on Ubuntu.
SERVICE="nginx"
case "$1" in
start)
echo "Starting $SERVICE..."
sudo systemctl start "$SERVICE"
;;
stop)
echo "Stopping $SERVICE..."
sudo systemctl stop "$SERVICE"
;;
restart|reload)
echo "Restarting $SERVICE..."
sudo systemctl restart "$SERVICE"
;;
status)
systemctl status "$SERVICE" --no-pager
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
The $1 is the first argument passed on the command line (see Command-line arguments below). The restart|reload line shows two words sharing one block. The final *) block prints a helpful usage message and exits with code 1 (a non-zero exit code means “something went wrong”) when the user types something unexpected.
Make it executable and run it:
chmod +x webserver-ctl.sh
./webserver-ctl.sh start
./webserver-ctl.sh wibble
Output:
Starting nginx...
Usage: ./webserver-ctl.sh {start|stop|restart|status}
The first call matched start). The second call (wibble) matched nothing, so the catch-all printed the usage line and exited.
Building a numbered menu
case also pairs nicely with a numbered menu. The select loop builds the menu for you, and case decides what each choice does:
#!/usr/bin/env bash
PS3="Choose an option: " # the prompt select shows
select choice in "Disk usage" "Memory usage" "Quit"; do
case "$choice" in
"Disk usage")
df -h /
;;
"Memory usage")
free -h
;;
"Quit")
echo "Goodbye."
break
;;
*)
echo "Invalid choice, try again."
;;
esac
done
Output:
1) Disk usage
2) Memory usage
3) Quit
Choose an option: 1
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 12G 36G 25% /
Best practices
- Always quote the tested value:
case "$1" in, nevercase $1 in. - End every block with
;;and never forget the finalesac. - Put a
*)catch-all last to handle unexpected input gracefully — print a usage message andexit 1. - Group equivalent inputs with
|(for examplerestart|reload) instead of repeating blocks. - Use character classes like
[Yy]*for case-insensitive yes/no prompts rather than listing every spelling. - Remember patterns are globs, not regex —
*means “anything”, and.is a literal dot, not “any character”. - Keep each block short; if a block grows large, move its logic into a function and call it from the case.