Skip to content

Exercise 6 of 10 · Pattern matching with case

Downloads Tidy-Up

What you will make

A six-line plan for tidying a downloads folder, one line per file, saying which folder each file should be moved to based on its extension.

The one new idea: case matches one value against glob patterns, each branch ending in ;;

Real shell scripts branch on the shape of a string all the time: which extension, which subcommand, which hostname. case does that in one readable block, using the wildcard patterns you already know from ls *.txt. Knowing that ;; is mandatory and that .txt is not the same pattern as *.txt will save you from two of the most common case bugs.

Go straight to the code ↓

Choosing by pattern

When you have one value and several possible shapes it could take, case reads better than a stack of if/elif. It compares a word against a list of patterns, top to bottom, and runs the commands for the first pattern that matches:

Shell
case "$day" in
  Sat|Sun) echo "weekend" ;;
  *)       echo "not weekend" ;;
esac

Three parts of the syntax are not optional. Each pattern ends with ). Each branch ends with ;;, which tells the shell "stop here, do not fall through to the next branch". And the whole thing closes with esac (case spelled backwards).

The patterns are the same wildcards you use for filenames: * matches anything, ? matches one character, and [a-z] matches one character from a range. That is why *.txt means "anything ending in .txt" while plain .txt means only the four-character string .txt. A | between patterns means "either of these". A final *) is the catch-all — it matches whatever nothing above it matched.

A worked example

Shell
for day in Mon Sat Wed Sun; do
  case "$day" in
    Sat|Sun) echo "$day: weekend" ;;
    Mon)     echo "$day: back to it" ;;
    *)       echo "$day: midweek" ;;
  esac
done
Output
Mon: back to it
Sat: weekend
Wed: midweek
Sun: weekend

Wed matched none of the named patterns, so it landed in *). The for loop feeding the case is the loop from the previous exercise, doing what it always does: one pass per word.

Your turn

The editor holds a plan for tidying a downloads folder. For each of six filenames it should print which folder the file belongs in: text and markdown files to Documents, .jpg to Pictures, .mp3 to Music, and anything else to Other.

Two lines are marked. The Documents pattern is missing its *, so .txt only matches a file literally named .txt, and every text file falls through to Other. The Pictures branch is missing its ;;, which is a syntax error — the shell will refuse to run anything until it is back.

Restore the ;; first, then fix the patterns, and run it.

If something goes wrong

syntax error near unexpected token ')' almost always means a branch is missing its ;;. Every branch needs one, even the last.

If notes.txt and README.md land in Other, the pattern is .txt where it should be *.txt — the star is what allows anything before the dot.

If nothing at all matches, check the esac is there and that each pattern ends with a ).

Write your code

Runs in your browser. Press Run (or Ctrl/Cmd+Enter) and the output is checked for you.

Ctrl/Cmd+Enter to run

Press Esc then Tab to move keyboard focus out of the code editor.

Ready
Output will appear here after you run your code.

The runtime is starting in the background. You can type now — it will be ready before you are.

The answer appears here once you have run your code at least once.

Things that often go wrong here

Leaving out the ;; at the end of a branch
The shell needs ;; to know where one branch stops and the next pattern begins. Without it the next line's pattern is read as part of the previous branch's commands, and the ) there is a syntax error that stops the whole script from running.
Writing .txt instead of *.txt
case patterns are matched against the whole word. .txt matches only the four-character name .txt. The * is what allows any number of characters before the dot.
Putting spaces around the | between patterns
*.txt | *.md is still parsed by bash, but it is easy to confuse with a pipe when reading, and some shells reject it. Keep the patterns and the | touching, as in *.txt|*.md.
Forgetting the catch-all *) branch
If no pattern matches, case simply does nothing and prints nothing, so invoice.pdf and setup.exe would silently vanish from the plan. A final *) is how you handle everything else.

Want a blank editor instead? Open the Bash & the Shell playground.