case is the clean way to dispatch on file types, subcommands and options.
Glob patterns match strings and file names: * any run of characters, ? exactly one character, [abc] or [a-z] one character from a set, [!0-9] one not in the set. When used unquoted as arguments, globs expand to matching file names, sorted; hidden files (starting with .) only match if the pattern starts with a dot.
The same patterns drive the case statement, which compares one word against a list of patterns and runs the first match:
case $word in
pat1|pat2) commands ;;
*) default ;;
esac| separates alternatives, ;; ends a branch, and *) is the catch-all. case is cleaner than a chain of elif [[ $x == ... ]] and it is POSIX, so it also works in sh.
Useful shell options via shopt -s:
nullglob: unmatched globs expand to nothing instead of themselves,globstar:**matches across directories, as in**/*.py,dotglob: include hidden files,extglob: extended patterns!(pat),@(a|b),+(pat),*(pat),?(pat).
Globs are not regular expressions: * alone means "anything", there is no . wildcard, and you need [[ =~ ]] for real regexes.
Example
cd "$(mktemp -d)"
for name in app.tgz main.c 2024.log --help notes.md; do
case $name in
*.tar.gz|*.tgz) kind="tarball" ;;
*.[ch]) kind="C source" ;;
[0-9]*) kind="starts with a digit" ;;
-h|--help) kind="help flag" ;;
*) kind="other" ;;
esac
echo "$name: $kind"
done
# app.tgz: tarball / main.c: C source / 2024.log: starts with a digit
# --help: help flag / notes.md: other
shopt -s nullglob extglob globstar
mkdir -p src/lib && touch a.log b.log c.txt src/x.py src/lib/y.py
logs=(*.log); echo "${#logs[@]} logs" # 2 logs
echo !(*.log|src) # c.txt
echo **/*.py # src/lib/y.py src/x.py
none=(*.zip); echo "${#none[@]}" # 0 (thanks to nullglob)Remember
- * any string, ? one char, [a-z] one char from a set
- case word in pat|pat) ... ;; esac picks the first matching pattern
- shopt -s nullglob, globstar and extglob unlock safer, richer globs
Watch out for
- Treating globs as regexes, e.g. expecting
.*to mean 'anything'.