Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 4 – Exit status, tests and branching

if, elif, else and case statements

Branch on any command's exit status with if, elif and else, match words against patterns with case, use fall-through safely, and know when a lookup table fits.

  • Beginner
  • 5 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 5 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write if, elif and else blocks around any command's exit status
  • Use case for pattern-based branching with alternatives, character classes and fall-through
  • Choose between case, if chains and an associative array for a given branching job

Before you start

What you’ll learn · Before you start

What you will learn

  • Write if, elif and else blocks around any command's exit status
  • Use case for pattern-based branching with alternatives, character classes and fall-through
  • Choose between case, if chains and an associative array for a given branching job

Before you start

On this page

if grep -q ERROR app.log; then needs no brackets at all: if runs a command and branches on its exit status, and [ ] is just one command among many. A case statement: A compound command, case word in pattern) commands ;; esac, that compares one word with glob patterns from the top and runs the commands of the first that matches. goes further and picks a branch by pattern, so a dispatcher with five branches reads as one table instead of a chain of tests.

You will write if, elif and else around any command, match words against patterns with case, use its fall-through terminators knowing which shells accept them, and recognise when a lookup table is clearer than either.

if runs a command and branches on its exit status

if list; then …; elif list; then …; else …; fi runs the first list and takes the then branch when it ends with status 0. Otherwise each elif list is tried in turn, and the else branch runs when none succeeded. The condition is any command: grep, cd, a function, (( )) or [ ]. The session branches on grep, on an arithmetic test and on cd, and ends with an if that takes no branch:

if and elif on grep, arithmetic and cd, and an if with no branch taken Python · if_session.py
from session import run, write

write("app.log", "INFO started\nWARN disk at 85 %\nINFO ready\n")
run("""
if grep -q ERROR app.log; then echo "errors found"; elif grep -q WARN app.log; then echo "warnings only"; else echo "clean"; fi
n=42; if (( n % 2 == 0 )); then echo "$n is even"; else echo "$n is odd"; fi
if cd nowhere 2>/dev/null; then echo "moved"; else echo "cd failed with $?, still here"; fi
if false; then echo "never printed"; fi; echo "no branch ran, status $?"
""")

Output

$ if grep -q ERROR app.log; then echo "errors found"; elif grep -q WARN app.log; then echo "warnings only"; else echo "clean"; fi
warnings only
$ n=42; if (( n % 2 == 0 )); then echo "$n is even"; else echo "$n is odd"; fi
42 is even
$ if cd nowhere 2>/dev/null; then echo "moved"; else echo "cd failed with $?, still here"; fi
cd failed with 1, still here
$ if false; then echo "never printed"; fi; echo "no branch ran, status $?"
no branch ran, status 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 if_session.py

The log had a WARN line but no ERROR, so the first grep failed, the elif grep succeeded, and the script printed warnings only. (( n % 2 == 0 )) tested an arithmetic condition directly. if cd nowhere took the else branch, and $? inside it was still 1, the status of the failed cd, because nothing ran in between. That is the advantage over running a command and testing $? later: the status is used where it is produced.

Each branch can hold any number of commands, and an if statement’s own status is that of the branch that ran, or 0 when no branch ran: the last line of the session tested false with no else, and $? was 0 afterwards, although the condition had failed. Bash ignores indentation; then may go on its own line or after a ;.

Brackets around a command

Wrong: if [ grep -q ERROR app.log ]; then, which hands grep to test as text and fails with an error. Right: if grep -q ERROR app.log; then. Brackets are needed only for a test, such as if [ -f "$file" ]; then.

Checkpoint

Check: if, elif and else

  1. Question 1 of 2 fruits.txt holds apple and mango. What does if grep -q kiwi fruits.txt; then echo yes; else echo no; fi print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: no

    if branches on the exit status of the command list after it, here 1 for no match.

  2. Question 2 of 2 app.log now has both an ERROR and a WARN line. Which branch of the session's if … elif … else runs?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: errors found, the first branch

    Put the most serious or most specific condition first, as the session does with ERROR before WARN.

case matches a word against patterns, from the top

case word in pattern) commands ;; … esac expands the word once and compares it with each pattern in order. The patterns use glob notation (*, ? and brackets), but they are compared with the word, never with file names. The first match runs its commands and ;; ends the statement; when no pattern matches, no branch runs and case ends with status 0, as an if does when no branch ran. | joins alternatives, and *), which matches anything, is the usual last case. The word is neither split nor expanded into file names, so case $word in needs no quotes, while a quoted pattern is taken literally: '*') matches only a star. classify.sh sorts each of its arguments into one of four kinds with one case inside a loop:

classify.sh: four kinds of word, tested from the top Shell · classify.sh
#!/bin/sh
# Classifies each argument with case: patterns are tried from the top, | joins alternatives, * is the default.
for word in "$@"; do
  case $word in
    apple | mango | banana) echo "$word: a fruit" ;;
    [[:upper:]]*) echo "$word: starts with a capital" ;;
    '' | *[!0-9]*) echo "$word: not a number" ;;
    *) echo "$word: a number" ;;
  esac
done

Step through its four patterns for four of the words the session gives it:

Interactive · step by step case tries its patterns from the top Step through four words. The patterns are on the left in order; the word and the branch it takes are on the right.
case tries its patterns from the top, step 5 of 5. Showing apple | mango | banana (no), [[:upper:]]* (no), '' | *[!0-9]* (no), * (matches), the word (4096), the branch taken (a number). In focus: *.apple | mango | bananano[[:upper:]]*no'' | *[!0-9]*no*matchesthe word4096the branch takena number

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. mango. The first pattern lists three alternatives joined by |. mango is one of them, so the first branch runs and case stops.
  2. Pune, first pattern. A new word starts again at the top. Pune is not apple, mango or banana, so the next pattern is tried.
  3. Pune, second pattern. [[:upper:]]* matches any word whose first character is a capital letter, whatever follows. Pune matches, so it is "starts with a capital".
  4. 12a, third pattern. 12a fails the first two patterns. [!0-9] matches a word containing at least one character that is not a digit, and the a is one, so 12a is "not a number".
  5. The catch-all. Only a word made of digits alone, such as 4096, gets past the third pattern. * matches anything, so it ends as "a number". Without it, such a word would run no branch at all.

The session runs classify.sh on five words in Bash and again in dash, then calls dispatch.sh, the dispatcher shown further down, three ways:

classify.sh in Bash and dash, and a subcommand dispatcher Python · case_session.py
from session import files, run

files("classify.sh", "dispatch.sh")
run("""
bash classify.sh mango Pune 4096 12a ''
dash classify.sh mango Pune 4096 12a ''
bash dispatch.sh start
!bash dispatch.sh restart
bash dispatch.sh --help
""")

Output

$ bash classify.sh mango Pune 4096 12a ''
mango: a fruit
Pune: starts with a capital
4096: a number
12a: not a number
: not a number
$ dash classify.sh mango Pune 4096 12a ''
mango: a fruit
Pune: starts with a capital
4096: a number
12a: not a number
: not a number
$ bash dispatch.sh start
starting the service
$ bash dispatch.sh restart
usage: dispatch.sh start|stop|status
[exit status 2]
$ bash dispatch.sh --help
usage: dispatch.sh start|stop|status

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 case_session.py

mango matched the alternatives apple | mango | banana. Pune matched [[:upper:]]*, any word that starts with a capital letter; a character class such as [:upper:] is safer than [A-Z], whose meaning can depend on the locale. 4096 reached the last case, and 12a and the empty word matched '' | *[!0-9]*: the empty word, or a word with any character that is not a digit. [!…] negates a bracket expression in POSIX patterns, and dash printed exactly the same five lines. Order matters: Pune is not a number either, yet it stopped at the second pattern, the first one that matched.

dispatch.sh is a typical use of case: one branch per subcommand, one for -h and --help, and a last *) branch for every other word, which prints the usage on standard error and ends the script with status 2:

dispatch.sh: one branch per subcommand Shell · dispatch.sh
#!/bin/bash
# A subcommand dispatcher: one case branch per command, a usage message and status 2 for anything else.
usage() { echo "usage: dispatch.sh start|stop|status" >&2; }
case ${1-} in
  start) echo "starting the service" ;;
  stop) echo "stopping the service" ;;
  status) echo "the service is running" ;;
  -h | --help) usage ;;
  *) usage; exit 2 ;;
esac

start ran its own branch. restart is not one of its subcommands, so it reached *), and the session recorded status 2 for it.

Key idea

case tries its patterns from the top and stops at the first match, so put specific patterns first and end with *) to catch everything else.

Checkpoint

Check: case and its patterns

  1. One question What does classify.sh print for the word Mumbai7?

    Choose one answer.

    Show the answer: One question

    Answer: Mumbai7: starts with a capital

    case tests patterns from the top and stops at the first match, so the order of the patterns is part of the logic.

Fall-through terminators, and when a lookup table is better

Besides ;;, a case clause can end with ;&, which also runs the next clause’s commands without testing its pattern, or ;;&, which goes on testing the later patterns. Bash added both in version 4.0, as its NEWS file records. POSIX.1-2024 adopted ;& as a standard terminator, but not ;;&, which stays a Bash extension. The session tries both, then a lookup:

;&, ;;&, dash's refusal, and an associative array Python · fallthrough_session.py
from session import run

# Bash 4.0 added two terminators: ;& runs the next body as well, and ;;& goes on testing the later patterns.
# POSIX.1-2024 made ;& standard, but dash 0.5.13.5 still refuses it; ;;& is Bash's own. An associative array
# replaces a case whose branches only map keys to values.
run("""
level=warn; case $level in error) echo "page someone" ;& warn) echo "write to the log" ;; *) echo "ignore" ;; esac
level=error; case $level in error) echo "page someone" ;& warn) echo "write to the log" ;; *) echo "ignore" ;; esac
f=report.pdf; case $f in *.pdf) echo "a PDF" ;;& report*) echo "a report" ;;& *) echo "checked every pattern" ;; esac
!dash -c 'case x in x) echo one ;& y) echo two ;; esac'
declare -A port=([http]=80 [https]=443 [ssh]=22)
echo "https is on port ${port[https]}"
""")

Output

$ level=warn; case $level in error) echo "page someone" ;& warn) echo "write to the log" ;; *) echo "ignore" ;; esac
write to the log
$ level=error; case $level in error) echo "page someone" ;& warn) echo "write to the log" ;; *) echo "ignore" ;; esac
page someone
write to the log
$ f=report.pdf; case $f in *.pdf) echo "a PDF" ;;& report*) echo "a report" ;;& *) echo "checked every pattern" ;; esac
a PDF
a report
checked every pattern
$ dash -c 'case x in x) echo one ;& y) echo two ;; esac'
dash: 1: Syntax error: "&" unexpected
[exit status 2]
$ declare -A port=([http]=80 [https]=443 [ssh]=22)
$ echo "https is on port ${port[https]}"
https is on port 443

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fallthrough_session.py

With level=error, ;& made the error branch fall into the warn branch, printing page someone and write to the log; with level=warn only the second line appeared. ;;& let report.pdf match three patterns in turn. dash 0.5.13.5 refused ;& with Syntax error: "&" unexpected, so a #!/bin/sh script that must run there should not use it, standard or not.

Which construct fits depends on what the branches compare. The third option in the table is an associative array: A Bash array whose subscripts are strings rather than numbers, such as capital[India]; declare -A creates one, it needs Bash 4.0, and its keys come back in no defined order. More in “Associative arrays: key-value maps”, which Bash has had since version 4.0 and POSIX sh lacks:

Branching job Best tool Why
Conditions on different commands or values if / elif each condition is its own command
One word against several patterns case patterns read as a table; works in every POSIX shell
Fixed keys mapped to values an associative array ${port[https]} is a lookup, not a branch

The last two lines of the session built one, declare -A port=([http]=80 [https]=443 [ssh]=22), and ${port[https]} gave 443 with no branching at all. When every branch of a case only assigns a value, a table says the same thing in less code and can grow without new branches.

Checkpoint

Check: Fall-through and lookup tables

  1. One question In case $level in error) echo "page someone" ;& warn) echo "write to the log" ;; esac, what prints for level=error?

    Choose one answer.

    Show the answer: One question

    Answer: page someone, then write to the log

    The session printed both lines for error and one for warn. dash 0.5.13.5 refuses ;& with a syntax error.

Practise: if and case

The questions use the log, the classifier and the dispatcher of the sessions with new words and values. Decide which branch runs before you check; each explanation ties the answer back to what the sessions showed:

Practice that adapts to you

Practise this lesson

11 questions about 3 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 What does if decide on?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: The exit status of the command list after if: 0 takes the then branch

    The session used grep, (( )) and cd as conditions.

  2. Question 2 of 11 In if cd nowhere 2>/dev/null; then …; else echo "cd failed with $?"; fi, what number does the else branch print?

    Type a number.

    Show the answer: Question 2 of 11

    Answer: 1

    Nothing runs between the failed cd and the else branch, so $? is still cd's status, 1.

  3. Question 3 of 11 What does if false; then echo yes; fi; echo $? print on its last line?

    Type a number.

    Show the answer: Question 3 of 11

    Answer: 0

    The condition failed and there is no else, so no branch ran, and an if that ran no branch ends with status 0. The session printed "no branch ran, status 0" for the same shape.

  4. Question 4 of 11 Which line correctly branches on whether app.log contains ERROR?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: if grep -q ERROR app.log; then

    Brackets are needed only when the condition is a test such as -f "$file".

  5. Question 5 of 11 What does a final *) branch do in a case statement?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: It matches any word that no earlier pattern matched

    dispatch.sh used it to print the usage and exit with 2 for unknown commands.

  6. Question 6 of 11 A clause reads y | Y | yes) echo ok ;;. Which input runs it?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: Y

    For any spelling of yes, a pattern such as [yY] | [yY][eE][sS] lists the alternatives explicitly.

  7. Question 7 of 11 What status does bash dispatch.sh Stop end with?

    Type a number.

    Show the answer: Question 7 of 11

    Answer: 2

    Patterns are case-sensitive, so Stop matches none of start, stop, status, -h and --help. The *) branch prints the usage on standard error and exits with 2, as it did for restart in the session.

  8. Question 8 of 11 Why does classify.sh print ": not a number" for an empty argument instead of ": a number"?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: The third pattern includes the alternative '', which matches the empty word before *) is reached

    Checking for a number by looking for a non-digit needs the empty case handled on its own; '' | [!0-9] does both.

  9. Question 9 of 11 What does ;;& do at the end of a case clause in Bash?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: It goes on testing the later patterns and runs every other clause that matches

    ;;& is Bash's own; POSIX.1-2024 standardised ;& but not ;;&.

  10. Question 10 of 11 A #!/bin/sh script uses ;& and runs on a system whose sh is dash 0.5.13.5. What happens?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: dash stops with a syntax error, although ;& is in POSIX.1-2024

    For scripts that must run under dash, repeat the commands or restructure the case instead of falling through.

  11. Question 11 of 11 A case maps 40 service names to port numbers, one assignment per branch. What is the better structure in Bash?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: An associative array, declare -A port=([http]=80 …), read with ${port[$name]}

    The session's ${port[https]} gave 443 with no branching. Keep case for patterns and real decisions.

Interview questions

Warm-up (fresher to 2–5 years): when would you use case instead of if and elif in a shell script?

Use case when one word is compared with several patterns: subcommands, file extensions, answers such as y | yes. Its patterns are globs, alternatives are joined with |, it tries them from the top and stops at the first match, and *) catches everything else, so a dispatcher reads like a table. It is POSIX, so it works in dash as well. Use if and elif when each branch tests something different: a file, a number, the status of a command. When every branch only maps a key to a value, an associative array is clearer than either.

Two harder questions follow, about a dispatcher that silently ignores unknown commands and about a case whose order sends inputs to the wrong branch. Decide what you would check first, then open each first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A deploy job runs service.sh restat, a typo for restart. The script's case has branches for start, stop and restart only. It printed nothing, the job reported success, and the old version kept running for an hour. Explain what happened and fix the script.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    An upload handler sorts files with case: .gz) runs gunzip, .tar.gz) runs tar -xzf, and *) leaves the file alone. Users report that backup.tar.gz ends up as backup.tar, never unpacked. Explain the cause, fix it, and say how you would stop the same mistake coming back.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does if actually test?

    The exit status of any command list: if grep -q ERROR app.log; then runs grep and branches on its status. Brackets are needed only when the condition is a test, such as [ -f "$file" ].

  2. How do elif and else decide which branch runs?

    if runs its first list; when it fails, each elif list is tried in turn, and else runs only when none of them succeeded.

  3. In which order does case try its patterns?

    From the top, and it runs only the first match, so specific patterns go first and *) at the end catches everything else. When nothing matches, case ends with status 0.

  4. Which pattern forms can a case clause use?

    Globs, alternatives joined with | such as y | yes, and bracket expressions such as [!0-9] or classes such as [[:upper:]], all of which work in every POSIX shell.

  5. What do ;& and ;;& do at the end of a case clause?

    ;& runs the next clause's commands without testing its pattern, and ;;& goes on testing the patterns that follow. Bash has both since 4.0; POSIX.1-2024 has ;&, but dash 0.5.13.5 refuses it.

  6. When is an associative array better than a long case?

    When the branches only map a key to a value, such as a service name to its port: one lookup replaces many clauses that each set a variable.

Key takeaways

  • if runs any command list and branches on its exit status, elif tries further lists in turn and else runs when none succeeded; brackets are needed only for a test.
  • case compares one word with glob patterns from the top and runs the first match, ending with status 0 when none matches; | joins alternatives, *) catches the rest, and [!…] and classes such as [:upper:] work in every POSIX shell.
  • ;& falls into the next clause and ;;& keeps testing; Bash has both since 4.0 and POSIX.1-2024 has ;&, but dash 0.5.13.5 refuses ;&.
  • Use if for different conditions, case for one word against patterns, and an associative array when branches only map keys to values.

Terms in this lesson

case statement
A compound command, case word in pattern) commands ;; esac, that compares one word with glob patterns from the top and runs the commands of the first that matches.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.