How to Use Case Statements in Bash

How to Use Case Statements in Bash

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" in opens 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 a break in 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.
  • esac closes 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|pattern2 matches 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 eval inside case blocks with user-supplied values, as that reintroduces the classic shell injection risk regardless of how the value was chosen.

Optimization Tips

  • case is generally faster than an equivalent if/elif chain 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 ;& or ;;& and running Bash 4.0 or later; earlier versions don’t support these operators. Check your version with bash --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 case for numeric range comparisons without character classes — case does string/glob matching, not numeric comparison, so [0-9] style classes or a helper if with -lt/-gt are 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
Total
2
Shares

Leave a Reply

Previous Post
How to Parse JSON in Bash

How to Parse JSON in Bash

Next Post
How to Create a Bash Password Generator

How to Create a Bash Password Generator

Related Posts