Globbing — the shell expanding * and ? into filenames — is older than every search tool you use, and it runs in a different place than people assume: the shell does the matching, before the command ever runs. Understanding that one fact explains all the surprises.
The patterns #
| Pattern | Matches |
|---|---|
* | Any string, including empty |
? | Exactly one character |
[abc] / [a-z] | One character from a set or range |
[!a-z] | One character NOT in the range |
{a,b}.conf | Brace expansion — not globbing, but same feel |
** | Recursive (with shopt -s globstar) |
$ ls *.log
$ cp report.{txt,pdf} archive/
$ rm !(*.keep) # extglob: everything except .keep
$ ls **/*.ts # every .ts at any depth, needs globstar
The surprise every script hits #
If nothing matches, bash leaves the pattern as literal text and the command receives *.log as an argument — which is why for f in *.log; do wc -l "$f"; done in an empty directory prints "wc: *.log: No such file". The glob failed; bash did not.
The fixes, in order of preference:
$ shopt -s nullglob # empty list instead of literal pattern
$ for f in *.log; do [ -e "$f" ] && wc -l "$f"; done # guard
$ ls *.log 2>/dev/null || echo "none" # tolerate
nullglob is the honest one for scripts; see globbing in loops for the full pattern.
Globs are not regex #
The two get conflated constantly. Globs match filenames with * = anything, ? = one char — there is no +, no alternation without extglob, no anchors because the whole filename must match. Regexes match text and live in grep and sed. The * is the same symbol doing a different job.
| You want | Use |
|---|---|
| Match filenames in a directory | Glob (*.log) |
| Match text patterns inside files | Regex with grep -E |
| Match paths at any depth | ** with globstar, or find |
| Complex filename matching | find -name / -regex |
In scripts #
Globbing inside a script obeys the same rules — and combining globs with arrays is the idiomatic collection step:
files=( *.log )
if [ -e "${files[0]}" ]; then
echo "${#files[@]} logs to process"
fi
When the listing gets structural — by age, size, type — find takes over from globs. The scripting series continues.
