Globs and wildcards
Globs are patterns the shell expands into matching filenames before any command sees them. * matches any string, ? matches one character, [...] matches a set, and {a,b} produces alternatives.
You want to delete every .log file in a directory, or list all .ts files that start with test. Typing each name individually is not an option. The shell provides glob patterns — a miniature matching language that expands into real filenames before a command ever runs. Getting this expansion order wrong is the number-one source of glob confusion.
By the end of this lesson you will know the four glob metacharacters, how the shell expands them, and where they silently do nothing.
After this lesson you can write glob patterns using *, ?, [...], and {...}, explain that the shell expands globs before passing arguments to the command, and avoid the classic “unquoted glob passed to the wrong tool” footgun.
The shell expands globs, not the command. This is the most important fact about globbing. When you type:
ls *.logThe shell looks at the current directory, finds every file whose name ends in .log, and replaces *.log with that list before calling ls. The ls program never sees the asterisk — it receives a list of actual filenames as separate arguments.
Consequence: if no file matches, bash (by default) passes the literal string *.log to the command, which usually causes an error or unexpected behavior. Set nullglob to get an empty list instead (covered in the Inset).
* matches any string of characters (including empty). It does not match a leading dot (hidden files) or a /.
ls *.txt # all .txt files in current dir
ls report_*.csv # report_2024.csv, report_final.csv, …
ls * # every non-hidden file in current dirCombining two * in a row produces ** which, with globstar enabled in bash, matches across directory boundaries. Without globstar, ** behaves like a single *.
? matches exactly one character. Useful when the length is fixed:
ls file?.txt # file1.txt, fileA.txt — NOT file10.txt
ls ???.sh # any .sh file with exactly a 3-char base nameEach ? consumes exactly one character. file?.txt does not match file.txt (zero chars before .txt) or file10.txt (two chars).
[...] matches one character from a set or range.
ls file[123].txt # file1.txt, file2.txt, file3.txt
ls [a-z]*.sh # any .sh file starting with a lowercase letter
ls [!0-9]* # files NOT starting with a digit (! negates)Ranges use ASCII order. [a-z] is lowercase letters; [A-Z] is uppercase; [0-9] is digits. Mixing can surprise you in non-C locales — stick to explicit lists or POSIX classes like [[:alpha:]] when in doubt.
{a,b,…} is brace expansion — it generates alternatives. Unlike the other globs, brace expansion does not require files to exist; it generates strings unconditionally:
echo file{1,2,3}.txt # file1.txt file2.txt file3.txt
cp config{,.bak} # copies config to config.bak (neat backup trick)
mkdir -p src/{en,ru}/cli # creates two directories in one commandBrace expansion happens before glob expansion. The shell first expands {a,b} into two strings, then checks each against the filesystem.
Cleaning up build artefacts.
Your project directory contains:
dist/app.js dist/app.js.map dist/app.css dist/app.css.map dist/README.mdGoal: delete all .map files and create a backup of both .js and .css originals.
# Delete map files
rm dist/*.mapAfter glob expansion the shell runs: rm dist/app.js.map dist/app.css.map
# Backup originals using brace expansion
cp dist/app{.js,.css} /tmp/backup/Brace expands to: cp dist/app.js dist/app.css /tmp/backup/ — two files, one command.
# Verify what remains
ls dist/app.js app.css README.md▸Why this works
Bash nullglob and failglob. By default, a glob with no matches is passed literally to the command (ls *.xyz → ls: cannot access '*.xyz'). Enable nullglob with shopt -s nullglob so unmatched globs expand to nothing (useful in scripts). Enable failglob with shopt -s failglob to make an unmatched glob an immediate error. For one-off interactive use, the default behavior is usually fine; in scripts, always choose one.
On macOS/BSD, zsh (the default shell since macOS Catalina) has nullglob-equivalent behavior on by default via NO_MATCH setting. Bash on macOS follows the same POSIX default as Linux bash.
▸Common mistake
Quoting a glob to pass it to a command that does its own matching. find and grep accept their own patterns, but the shell will expand unquoted globs first. This command is almost always wrong:
find . -name *.log # WRONG: shell may expand *.log first
find . -name "*.log" # CORRECT: shell passes literal *.log to findIf the current directory has .log files, the first form might work by luck. If it has exactly one, find receives a filename, not a pattern. Always quote patterns passed as arguments to commands that interpret them internally.
You run: rm *.tmp. Your directory has no .tmp files. What happens by default in bash?
Glob patterns are expanded by the shell, not by the command that follows. The four metacharacters are: * (any string), ? (one character), [...] (one character from a set), and {a,b} (brace expansion — generates alternatives unconditionally before filesystem matching). The expansion order is: braces first, then filesystem globs. Always quote patterns you intend to pass to commands like find or grep that do their own matching internally — otherwise the shell expands them first and the command sees filenames instead of patterns.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.