iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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.

Example

Example
ls *.md
ls src/**/*.js          # globstar (set -o globstar)
rm photo[0-9].jpg
Try it Yourself »

Discussion

Loading…