Wildcards & Glob
Globs (*, ?, [...], {...}, **) expand to file paths BEFORE commands see them — that’s why rm *.tmp deletes a.tmp + b.tmp and not a file literally named *.tmp. Knowing the rules saves you from accidentally deleting everything.
* ? [ ] { } **, quoting, find
EXAMPLE
# 1) The basics
ls *.md # all files ending in .md
ls ?.txt # single-char filenames + .txt: a.txt, b.txt, but NOT ab.txt
ls [abc]*.log # files starting with a, b, or c, then .log
ls [a-z]*.log # any lowercase letter
ls [!a-c]*.log # NOT a, b, or c
# 2) Brace expansion — NOT a glob; expanded by shell first
ls *.{md,txt,rst} # *.md, *.txt, *.rst — three globs
cp file.txt file.txt.bak # backup
cp file.txt{,.bak} # same — { } expands to two args
mkdir -p {dev,test,prod}/{config,logs} # cartesian product → 6 dirs
echo img{01..05}.jpg # img01.jpg img02.jpg … img05.jpg (zero-padded)
echo {a..z} # a b c … z
echo {1..10..2} # 1 3 5 7 9 (step)
# Brace expansion runs even when there's no matching file — it's pure string substitution.
# 3) Recursive glob — **
shopt -s globstar # bash; on by default in zsh
ls **/*.ts # all .ts files at any depth
rm -i **/*.log
find . -name '*.ts' # alternative without globstar
# 4) Hidden files — globs DON'T match dotfiles by default
ls .* # explicit dotfile glob
shopt -s dotglob # make * include dotfiles
# 5) Quoting protects from expansion
echo *.tmp # SHELL expands; ls sees expanded list
echo "*.tmp" # ls sees the literal string
find . -name '*.tmp' # PASS the pattern to find — quote so shell doesn't expand
# 6) When no match exists
ls *.foo # default: 'no such file' (bash treats unmatched glob as literal)
shopt -s nullglob # unmatched globs expand to NOTHING (safer for scripts)
shopt -s failglob # unmatched globs are an error
# In scripts that loop over files: always use nullglob to avoid surprises:
for f in *.log; do
[ -e "$f" ] || continue # belt-and-braces if nullglob not set
process "$f"
done
# 7) extglob — extended patterns (bash)
shopt -s extglob
ls !(*.bak) # all files NOT ending in .bak
ls *.@(jpg|jpeg|png) # one of jpg/jpeg/png
ls *.+([0-9]) # one or more digits at end
ls *.?(test|spec).ts # optional infix
# 8) zsh extras (way richer than bash)
ls **/*.ts(.) # only regular files (.); skip dirs/symlinks
ls **/*.ts(om[1]) # newest .ts file by mtime
ls **/*.log(.L+100) # > 100 KB
ls **/*~*.test.ts # all .ts EXCEPT .test.ts
# 9) Using globs in find vs ls
find src -type f -name '*.ts' -mtime -7
find . -type d -empty -delete
find . -name '*.bak' -print -delete
# 10) Replacement tools — fd is glob-friendly and respects .gitignore
fd '\.ts$' src/ # regex, not glob
fd -e ts # files with extension ts
fd -t f -t d # specific types
fd -g '*.test.ts' # explicit glob mode
# 11) Real-world recipes
# Convert images
for f in *.png; do convert "$f" "${f%.png}.webp"; done
# Move .log files older than 30 days
for f in **/*.log; do
[ "$(find "$f" -mtime +30 -print)" ] && mv "$f" /archive/
done
# Bulk rename
for f in IMG_*.JPG; do mv "$f" "${f,,}"; done # lowercase (bash 4+)
# 12) Patterns to AVOID in destructive commands
rm -rf * # nuke everything in cwd; DEMONSTRATE in a sandbox
rm -rf .* # ALSO matches '..' on some old shells — can climb out!
rm -rf -- * # the '--' is good hygiene: prevents files named '-rf' as flags
# Always:
# • test with 'ls' or 'echo' first
# • use --dry-run flags when available
# • be careful with quotes; un-quoted variables can expand to globs unexpectedly
# 13) Common bugs
# • Forgot to quote variable holding a path with spaces or *. → expanded against cwd
# • for f in *.log without nullglob — loop runs once with literal '*.log' when no matches
# • Globbing with sudo — sudo runs the command after shell expansion; pass the right paths or quote
# • dotglob unintentionally enabled in interactive shell — every * picks up .git, .env, etc.
# • Using regex syntax in globs — globs aren't regex; * means any chars, not 'one or more'
# • Brace expansion limits — bash has a max expansion size; massive ranges fail silently
# • Newline-containing filenames break for-loops — use find -print0 / xargs -0 / mapfile
# • IFS confusion — set IFS=$'\n\t' at top of scripts to avoid splitting on spaces
Why it matters
Globs run BEFORE the command sees its args — ls *.md expands first, then ls receives the file list. Pair shopt -s globstar nullglob at the top of scripts so **/*.ts works and unmatched patterns don’t pass through as literals. Quote patterns you want a command (like find -name) to handle itself.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…