The VikiD Truth

Knowledge base / Computing / Linux & the command line

Quicklinks | Read This! | Picolisp | Picolisp Machine | Pil Sources | Pil Tutorials | Linux | BASH | C-Programmming | Javascipt | Python | Scheme | Operating Systems | AssemblyLanguage | Computer Security | Firewalls | Exploitation | Social Engineering | Metasploit | Emacs | vim | Pharo Smalltalk | Databases | Networking | Machine Learning | Git | Machine Learning | Algorithms | Open Data Science | Vikid’s Bash Cookbook |

Bash case statements

Here is the truth about case in Bash: it is not a weaker if – it is a pattern matcher, and patterns are where it earns its keep. When you find yourself writing a chain of [[ $x == … ]] branches, case is usually clearer.

What it is, in plain words

case matches a string against a list of glob patterns and runs the commands for the first match:

case "$word" in
  pattern1) commands ;;
  pattern2|pattern3) commands ;;
  *) commands ;;          # default
esac

Patterns are the same style as filename globs: * any string, ? any one character, [abc] a set, and with shopt -s extglob, richer forms like +(foo|bar).

Why it matters

Option parsing, file-type dispatch, answering “what kind of argument is this?” – case expresses these without nested ifs. It is also POSIX, so scripts using it run under dash and other /bin/sh implementations. A surprising amount of system init and package-manager scripting is case all the way down.

An everyday analogy: a sorting desk

Letters arrive. A clerk glances at the postcode pattern and drops each into a bin: “starts with SW”, “ends with 4AE”, “everything else”. The clerk does not open the letters (if with complicated tests); they match the address against stencils. That’s case.

A hands-on example

#!/bin/bash
# greet.sh
name=${1:-world}
case "$name" in
  [Vv]ikid|VikiD) echo "Hello, me." ;;
  vikix|void)     echo "Hello, machine." ;;
  *.local)        echo "Hello, LAN host $name" ;;
  "")             echo "Hello, nobody?" ;;
  *)              echo "Hello, $name." ;;
esac

File-extension dispatch:

case "$file" in
  *.tar.gz|*.tgz) tar -tzf "$file" ;;
  *.tar.xz)       tar -tJf "$file" ;;
  *.zip)          unzip -l "$file" ;;
  *.md)           "${EDITOR:-nvim}" "$file" ;;
  *)              echo "unknown type: $file" >&2 ; exit 1 ;;
esac

Simple option parsing (for fuller needs, use getopts):

while [ $# -gt 0 ]; do
  case "$1" in
    -v|--verbose) verbose=1 ; shift ;;
    -o|--output)  output=$2 ; shift 2 ;;
    --)           shift ; break ;;
    -*)           echo "unknown: $1" >&2 ; exit 1 ;;
    *)            break ;;
  esac
done

;;& (Bash) falls through to test the next pattern; ;& falls through and runs the next arm. Prefer explicit patterns joined with | unless you need fall-through.

Patterns in detail

# character classes and ranges
case "$c" in
  [a-z]) echo lower ;;
  [A-Z]) echo upper ;;
  [0-9]) echo digit ;;
esac

# extglob (Bash)
shopt -s extglob
case "$file" in
  +(*.jpg|*.png)) echo image ;;
  !(*.tmp))       echo "not a temp file" ;;
esac

Nested case is fine; so is calling a function from each arm to keep the structure readable. When the patterns are really regular expressions, step up to [[ $s =~ ^[0-9]+$ ]] or pipe through grep -E – don’t force case to be something it isn’t.

case versus if

Use if when the test is a condition (-f file, -z "$s", numeric comparison). Use case when the test is “which of these shapes does this string match?”. Mixing them – case for the shape, if inside an arm for a deeper check – is normal and clear.

Common misconceptions

A real script: options with getopts, commands with case

getopts handles the flags; case dispatches on each one. Then a second case picks a sub-command, the same shape as git or xbps-*:

#!/usr/bin/env bash
# notes - a tiny note tool
set -euo pipefail
dir=${NOTES_DIR:-$HOME/notes}
verbose=0

usage() { echo "usage: notes [-v] [-d dir] {add|list|grep} [args]" >&2; exit 2; }

while getopts ':vd:h' opt; do
  case $opt in
    v)  verbose=1 ;;
    d)  dir=$OPTARG ;;
    h)  usage ;;
    :)  echo "-$OPTARG needs a value" >&2; usage ;;
    \?) echo "unknown option -$OPTARG" >&2; usage ;;
  esac
done
shift $((OPTIND - 1))

cmd=${1:-list}; shift || true
mkdir -p "$dir"

case $cmd in
  add)       printf '%s %s\n' "$(date +%F)" "$*" >> "$dir/inbox.txt" ;;
  list|ls)   cat -- "$dir/inbox.txt" 2>/dev/null || echo "(empty)" ;;
  grep|find) grep -i -- "${1:?search term}" "$dir/inbox.txt" ;;
  *)         usage ;;
esac
(( verbose )) && echo "done: $cmd in $dir" >&2
exit 0

Notice three case habits here: | to give a command aliases (list|ls), \? escaped because a bare ? is a glob matching any single character, and a final *) that catches everything else so no input falls through silently.

Terminators: ;;, ;& and ;;&

Most people only know ;; (stop here). Bash 4 added two more:

TerminatorEffect
;;stop; no more patterns are tried
;&fall through and run the next branch’s commands unconditionally (like C without break)
;;&keep testing the following patterns, running every one that also matches
case $file in
  *.tar.*) echo "a tarball" ;;&
  *.gz)    echo "gzip compressed" ;;
  *.xz)    echo "xz compressed" ;;
esac
# backup.tar.gz prints "a tarball" then "gzip compressed"

The PicoLisp equivalent of case is also called case and matches values rather than glob patterns: (case Cmd ("add" (addNote)) (("list" "ls") (listNotes)) (T (usage))). For pattern matching on structure, PicoLisp has match.

Connections

#!/bin/bash

CHAR=$1

case $CHAR in
  [a-z])
        echo "Small Alphabet." ;;
  [A-Z])
        echo "Big Alphabet." ;;
  [0-9])
        echo "Number." ;;
  *)
        echo "Special Character."
esac

Last edited · 2 revisions

Tags: #Computing