Globbing is the shell’s pattern matching for file names. When you type rm *.log, it isn’t rm that understands *: the shell expands the pattern into a list of matching files, then runs the command. Understanding this makes you faster, and helps avoid dangerous mistakes.
The basic wildcards
| Pattern | Matches | Example |
|---|---|---|
* |
Any number of characters (including none) | *.log → app.log, error.log |
? |
Exactly one character | file?.txt → file1.txt, fileA.txt |
[abc] |
One character from the set | log[123].txt |
[a-z], [0-9] |
One character in a range | backup-[0-9][0-9].tgz |
[!abc] or [^abc] |
One character not in the set | [!.]* → names not starting with a dot |
ls *.conf
ls access.log.? # access.log.1 … access.log.9
ls backup-2026-0[1-6]-*.tgz # first half of 2026
Globs don’t match hidden files (starting with .) unless the pattern starts with a dot. See Linux hidden files.
Brace expansion (not a glob, but related)
Braces generate strings, whether or not files exist:
mkdir -p project/{src,tests,docs}
cp nginx.conf{,.bak} # cp nginx.conf nginx.conf.bak
echo file{1..5}.txt # file1.txt … file5.txt
ls /var/log/{syslog,auth.log}
Character classes
ls *[[:digit:]]* # names containing a digit
ls [[:upper:]]* # names starting with an uppercase letter
Other classes: [:alpha:], [:alnum:], [:space:], [:lower:].
Recursive globs and options (Bash)
shopt -s globstar # enable **
ls **/*.php # all .php files in all subdirectories
shopt -s dotglob # * also matches hidden files
shopt -s nullglob # a pattern with no matches expands to nothing (instead of itself)
shopt -s extglob # extended patterns
ls !(*.gz) # everything except .gz files (extglob)
For large trees, find is often clearer and more powerful. See Linux find command.
Quoting: when you don’t want expansion
echo *.txt # expanded by the shell
echo "*.txt" # literal *.txt
find /var/log -name "*.gz" # quote it, so find (not the shell) does the matching
grep "error.*timeout" app.log # regex for grep, quoted so the shell leaves it alone
Unquoted patterns in find -name are a classic bug: if a matching file exists in the current directory, the shell expands it first.
Wildcards (globs) are not regular expressions: in a glob * means “anything”, while in a regex .* does. See regex basics.
Safety with rm, mv and cp
- Preview first: replace the command with
echoorls:
echo rm /var/www/site/cache/*
ls -d /tmp/build-*
- Watch for stray spaces:
rm -rf /var/www/site/ *deletes the current directory’s contents too. - Use
--before patterns that might start with a dash:rm -- *.tmp. - With variables, quote and guard them:
rm -rf "${DIR:?}/"*fails ifDIRis empty, instead of deleting/*. - “Argument list too long” means the glob matched too many files. Use
find … -deleteorxargs.
find /var/www/site/cache -type f -name '*.tmp' -delete
Globs in scripts
In scripts, an unmatched glob stays as the literal text (*.log), so a loop over for f in /var/log/app/*.log runs once with a non-existent file name when the directory is empty. Either enable nullglob, or check that each file exists inside the loop ([ -e "$f" ] || continue). Always quote "$f" when using the variable, so file names with spaces work.
Quick reference
| Pattern | Meaning |
|---|---|
* |
anything |
? |
one character |
[0-9] |
one digit |
[!x] |
not x |
{a,b} |
a or b (brace expansion) |
** |
recursive (shopt -s globstar) |
"*.log" |
literal (quoted) |
Related: Bash scripting tutorial and pipes and redirection.