Bash — The `case` Conditional Statement

Learn how Bash’s case statement makes conditional branching cleaner and easier to read. Explore patterns, wildcards, multiple matches, defaults, and practical examples—and discover why case is often a better choice than a maze of if and elif.

Bash — The `case` Conditional Statement

When a Bash script needs to choose between several possible actions, case is often a much cleaner solution than a long chain of if / elif statements.

If you have programmed in other languages, you may recognize it as the Bash equivalent of a switch statement.

The basic idea is simple: Bash evaluates a value against a series of patterns. When a pattern matches, the corresponding commands are executed.

The general syntax looks like this:

case value in
    pattern1)
        cmds1
        ;;
    pattern2)
        cmds2
        ;;
    *)
        default_cmds
        ;;
esac

The case statement starts with case and ends with esac — which is simply case spelled backwards. Bash has apparently decided that symmetry is important.

Each pattern is followed by ), then the commands to execute, and finally ;; to mark the end of that branch.

The * pattern is commonly used as the default case because it matches anything that hasn't matched an earlier pattern.

A Real Example

Here's a small example that demonstrates how case can be used together with a loop:

while :
do
    printf "Type . to finish ==> "
    read line

    case "$line" in
        .)
            echo "Message done"
            break
            ;;
        *)
            echo "$line" >> "$message"
            ;;
    esac
done

The while : creates an infinite loop. The : is Bash's null command, which always succeeds, so the loop continues until something explicitly breaks it.

Inside the loop, the script waits for the user to enter a line.

The case statement then checks what was entered:

case "$line" in
    .)
        ...
        ;;
    *)
        ...
        ;;
esac

If the user enters a single dot:

.

the first pattern matches:

.)
    echo "Message done"
    break
    ;;

The script prints:

Message done

and break terminates the loop.

For anything else, the * pattern matches:

*)
    echo "$line" >> "$message"
    ;;

The entered text is appended to the file referenced by $message.

So the overall behavior is quite straightforward:

Type . to finish ==> Hello
Type . to finish ==> This is a message
Type . to finish ==> Another line
Type . to finish ==> .
Message done

The loop keeps accepting input until the user enters a single period. Everything else is treated as message content.

Why Use case?

You could implement the same logic with if and elif:

if [[ $line == "." ]]; then
    echo "Message done"
    break
else
    echo "$line" >> "$message"
fi

For two possibilities, that is perfectly reasonable.

But as the number of alternatives grows, case becomes much easier to read:

case "$command" in
    start)
        start_service
        ;;
    stop)
        stop_service
        ;;
    restart)
        restart_service
        ;;
    status)
        show_status
        ;;
    *)
        echo "Unknown command: $command"
        exit 1
        ;;
esac

This is one of the situations where case really shines: the possible values and their corresponding actions are visible at a glance.

Patterns, Not Just Exact Values

One important detail is that Bash case works with patterns, not just literal string comparisons.

For example:

case "$filename" in
    *.log)
        echo "Log file"
        ;;
    *.conf)
        echo "Configuration file"
        ;;
    *.sh)
        echo "Shell script"
        ;;
    *)
        echo "Unknown file type"
        ;;
esac

Here:

*.log

matches filenames ending in .log, while:

*.sh

matches shell scripts.

You can also combine multiple patterns in a single branch:

case "$environment" in
    production|staging)
        echo "Remote environment"
        ;;
    development|testing)
        echo "Non-production environment"
        ;;
    *)
        echo "Unknown environment"
        ;;
esac

This is often much cleaner than writing several separate conditions.

One More Useful Example

A common use for case is processing command-line arguments:

#!/usr/bin/env bash

case "${1:-}" in
    start)
        echo "Starting service..."
        ;;
    stop)
        echo "Stopping service..."
        ;;
    restart)
        echo "Restarting service..."
        ;;
    status)
        echo "Checking service status..."
        ;;
    *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac

You could then run:

./service.sh start

or:

./service.sh status

An unknown command falls through to the * branch.

This pattern appears everywhere in shell scripting, from small utility scripts to much larger command-line tools.

A Small Syntax Detail Worth Remembering

Don't forget the ;; at the end of each branch:

case "$value" in
    one)
        echo "One"
        ;;
    two)
        echo "Two"
        ;;
esac

Unlike some languages, Bash does not use break to terminate an individual case branch. The ;; tells Bash that the commands belonging to that pattern are finished.

And unlike a traditional C-style switch, Bash does not normally fall through from one matching branch into the next.

case vs. if

As a general rule:

Use if when you're asking a question:

if [[ -f "$file" ]]; then
    echo "File exists"
fi

Use case when you're choosing an action based on one value:

case "$action" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        restart
        ;;
esac

Of course, Bash doesn't enforce this distinction. You can solve the same problem in several ways. The goal is to choose the construct that makes the script easiest for the next human to understand; which may, unfortunately, be you six months from now.

Summary

The Bash case statement is the shell's answer to the classic switch construct found in many programming languages.

Its basic structure is:

case "$value" in
    pattern1)
        commands
        ;;
    pattern2)
        commands
        ;;
    *)
        default_commands
        ;;
esac

It is particularly useful when:

  • you have several possible values;
  • each value should trigger a different action;
  • you want to use patterns such as *.log;
  • several patterns should share the same action;
  • you want command-line argument handling to remain readable.

For simple two-way decisions, if is often perfectly adequate. When your script starts looking like a wall of elif, however, it may be time to let case do the heavy lifting.

And, as always with Bash, remember: the shell may look simple right up until it isn't.

Read next

Bash — The Built-in [[ ]] Conditional Expression

Discover Bash's powerful `[[ ]]` conditional expression: safer string and numeric comparisons, pattern matching, regular expressions, file tests, and logical operators—without the common pitfalls of `[ ]` and `test`

Bash !: Negating Command Results in Bash

Master the Bash ! operator to easily reverse exit statuses and trigger actions when a command fails. This quick guide breaks down how the logical NOT operator transforms success into failure (and vice versa) with practical scripting examples.