Wildcards and Globbing
When you type ls *.txt at the command line, Bash — not ls — is the program deciding which files that command actually sees. This automatic pattern matching against filenames is called globbing, and the special characters that trigger it (*, ?, [...]) are called wildcards. Wildcards let you refer to many files at once without typing every name individually, and they are one of the most-used features of daily shell work. Misunderstanding exactly how and when they expand is also one of the most common sources of shell bugs, including some genuinely destructive ones — so this lesson covers not just the syntax, but the mechanics underneath it.
Overview: How Globbing Actually Works
Before Bash runs any command, it processes the command line through a series of expansion steps, in a fixed order: brace expansion, tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, word splitting, and finally pathname expansion — which is the technical name for globbing. This happens near the very end of that pipeline, and critically, it happens entirely inside the shell, before the command you typed ever runs.
This matters more than it sounds like it should. Programs like ls, rm, and cp never see the pattern *.txt at all — they only ever see a plain list of filenames that Bash has already substituted in. You can prove this to yourself with echo, a program that does nothing except print its arguments back: echo *.txt prints the matching filenames, not the literal string *.txt, because Bash expanded the glob before echo ever started running. The wildcard characters are a shell feature, not a feature of the individual commands you run.
Mechanically, when Bash encounters an unquoted word containing *, ?, or a bracket expression, it reads the entries of the relevant directory (via the C library’s readdir/glob() routines under the hood), tests each entry’s name against the pattern, and replaces the pattern in the command line with the sorted list of matches, each as a separate argument. If nothing matches, Bash’s default behavior is to leave the pattern text unchanged and pass it to the command literally — which is why an unmatched glob often shows up in an error message that still contains the asterisk.
Two behaviors surprise almost everyone at some point. First, by default * does not match filenames that start with a dot — hidden files like .bashrc or the special entries . and .. are skipped unless you opt in with the dotglob shell option. This is a shell convention for safety and convenience, not a filesystem rule; the kernel has no concept of a “hidden” file. Second, Linux filesystems (ext4, XFS, Btrfs, and so on) are case-sensitive, so *.jpg will not match photo.JPG — Windows and macOS users switching to Linux trip over this constantly.
Globbing is also fundamentally different from regular expressions, even though some of the same characters appear in both. In a glob, * means “zero or more of any character”; in a regex, * means “zero or more of the previous character.” Tools like grep, sed, and awk use regex, not glob syntax — only the shell itself, and a handful of glob-aware utilities, interpret wildcards the way this lesson describes.
Syntax
Wildcards are used directly as part of a filename argument — there’s no special command needed to invoke them:
command pattern-with-wildcards
| Wildcard | Meaning |
|---|---|
* |
Matches zero or more characters (but not a leading dot in a hidden filename, by default) |
? |
Matches exactly one character |
[set] |
Matches exactly one character that is a member of set, e.g. [abc] or a range like [0-9] |
[!set] or [^set] |
Matches exactly one character that is not in set |
[[:class:]] |
POSIX character classes inside a bracket expression, e.g. [[:digit:]], [[:alpha:]], [[:upper:]] |
{a,b,c} |
Brace expansion — technically a separate, earlier expansion step, not pathname globbing (it works even when no matching files exist) |
** |
With shopt -s globstar enabled, matches files and directories recursively through subdirectories |
Examples
Example 1: Matching by extension with *
cd ~/documents
ls
invoice.pdf notes.txt photo.jpg report.txt todo.txt
ls *.txt
notes.txt report.txt todo.txt
Bash expanded *.txt into the three matching filenames before ls ever ran, so ls only ever saw ls notes.txt report.txt todo.txt as its argument list.
Example 2: Proving it’s the shell, not the command
echo *.txt
notes.txt report.txt todo.txt
echo has no idea what a wildcard is — it just prints its arguments. Getting the same filenames back confirms the expansion happened in Bash before echo was invoked.
Example 3: Precise matching with ? and bracket expressions
cd /var/log/app
ls
access1.log access2.log access3.log access10.log error.log
ls access?.log
access1.log access2.log access3.log
? matches exactly one character, so access10.log (two digits) is correctly excluded. Narrowing further with a bracket range:
ls access[1-3].log
access1.log access2.log access3.log
This is more explicit than access?.log — it says exactly which digits are acceptable, which matters once files like access9.log or accessX.log might exist.
How It Works Step by Step
Take a realistic backup command and trace what Bash does before anything runs:
mkdir -p ~/backups
cp /var/log/app/*.log ~/backups/
ls ~/backups
Step by step, for the cp line:
- Bash tokenizes the line into words:
cp,/var/log/app/*.log,~/backups/. ~/backups/undergoes tilde expansion first, becoming an absolute path like/home/ana/backups/.- Bash sees the unquoted
*in/var/log/app/*.logand performs pathname expansion: it lists the entries in/var/log/app/, keeps the ones ending in.log(skipping any dotfiles), and sorts them. - The single word
/var/log/app/*.logis replaced with however many matching paths were found, each becoming its own argument. - Only now does Bash actually run
cp, viafork()andexecve(), handing it the fully expanded argument list —cpitself never sees an asterisk. cpcopies each matched file into~/backups/, and the finallsshows the result.
Note that brace expansion is different and happens even earlier, purely as text substitution — it doesn’t check the filesystem at all:
touch config_{dev,staging,prod}.yaml
This becomes touch config_dev.yaml config_staging.yaml config_prod.yaml before touch runs, and it works even though none of those files exist yet — unlike a real glob, which can only expand to files that already exist.
Common Mistakes
Mistake 1: An overly broad glob in a destructive command
A classic and genuinely damaging mistake is running a wildcard delete without checking what it actually matches:
cd /var/log/app
rm *
The intent might have been to remove only old .tmp files, but * matches every non-hidden file in the current directory, so everything — including files still in use — is gone, and there’s no undo. Always confirm what a pattern matches before it touches rm, and be as specific as the situation allows:
cd /var/log/app
ls *.tmp
rm *.tmp
Running ls with the same pattern first shows exactly what rm would delete, with zero risk.
Mistake 2: Assuming * matches hidden files
A script written to process “every file” in a directory silently skips dotfiles, because * doesn’t match them by default:
for f in *; do
echo "Processing: $f"
done
If the directory contains .env or .gitignore, this loop never touches them. If hidden files genuinely need to be included, enable dotglob explicitly so the intent is visible in the script:
shopt -s dotglob
for f in *; do
echo "Processing: $f"
done
shopt -u dotglob
Mistake 3: Treating glob syntax as regex syntax
Wildcards work for filename arguments to the shell, but tools like grep interpret their pattern argument as a regular expression, where the same characters mean something different:
grep 'report*.csv' filelist.txt
Here * means “zero or more of the character right before it” — so this pattern matches strings like repor, report, or reporttt.csv, not “report followed by any characters” as a glob would. To search for filenames matching that shape inside a text file, use a real regex:
grep -E 'report.*\.csv' filelist.txt
And to actually match filenames on disk, skip grep and let the shell glob directly: ls report*.csv.
Best Practices
- Before running a glob inside
rm,mv, or any other destructive command, run the identical pattern throughlsorechofirst to see exactly what it matches. - Prefer the most specific pattern that does the job — a bracket range or explicit extension is safer than a bare
*. - In scripts that loop over glob results, enable
shopt -s nullglobso an unmatched pattern expands to nothing instead of being passed through as a literal, unmatched string. - Enable
shopt -s dotglobonly when you deliberately need hidden files included, and turn it back off afterward if the rest of the script assumes the default behavior. - Use
shopt -s globstarand**when you need to match recursively through subdirectories, instead of shelling out tofindfor simple cases. - Always quote a variable that holds a filename (
"$file"), even though this lesson is about unquoted wildcard characters — the two are different: a wildcard character is deliberately left unquoted to trigger expansion, while a variable holding an already-known filename should stay quoted to prevent accidental re-expansion or word splitting. - Remember that glob patterns and regular expressions look similar but mean different things — don’t reuse a glob pattern as a
greporsedargument without translating it.
Practice Exercises
- In a directory containing a mix of file types, write a single command that lists only files ending in
.csvor.tsv. Hint: combine brace expansion with a glob, e.g. a pattern shaped like*.{csv,tsv}. - Write a script fragment that loops over
*.logfiles in/var/log/appand prints a friendly message instead of an error if none exist. Hint: this is exactly whatshopt -s nullglobis for. - Given files
access1.logthroughaccess9.log, write a wildcard pattern that matches onlyaccess1.logthroughaccess5.log. Hint: a bracket range with two digits won’t work here — think about what a single-character range like[1-5]covers.
Summary
- Globbing (pathname expansion) happens inside Bash before a command runs — the command itself only ever sees the already-matched filenames, never the wildcard characters.
*matches zero or more characters,?matches exactly one, and[...]/[!...]match or exclude one character from a set.- By default,
*does not match dotfiles, and Linux filename matching is case-sensitive. - Brace expansion (
{a,b,c}) is a separate, earlier text-substitution step — it works even for files that don’t exist yet, unlike a true glob. - An unmatched glob pattern is passed through literally unless
nullglobis enabled, which is a common source of confusing script errors. - Glob syntax and regular-expression syntax use the same characters for different meanings — don’t confuse a shell wildcard with a
grep/sedpattern. - Always preview a wildcard pattern with
lsorechobefore using it in a destructive command likerm.
