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 #

PatternMatches
*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}.confBrace expansion — not globbing, but same feel
**Recursive (with shopt -s globstar)
Globs you will type this week
$ 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:

Three ways to handle no matches
$ 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 wantUse
Match filenames in a directoryGlob (*.log)
Match text patterns inside filesRegex with grep -E
Match paths at any depth** with globstar, or find
Complex filename matchingfind -name / -regex

In scripts #

Globbing inside a script obeys the same rules — and combining globs with arrays is the idiomatic collection step:

Collect matches, then decide
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.