| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user; no root required |
| Scope | Bash pathname expansion (globbing): *, ?, [...], no-match behavior, nullglob, failglob, dotglob, nocaseglob, globstar, extglob, quoting, arrays, and safe loops. Does not cover regular-expression matching with the =~ operator or the full shell expansion order. |
| Related guides | Bash shell script tutorial Shell scripting course hub |
Bash globbing expands wildcard patterns such as *.log into matching pathnames before a command runs. The examples below use a disposable directory and printf so you can see exactly what Bash expands.
Globbing is pathname expansion, not regex. When you know how no-match behavior, hidden files, and shopt options change the result, loops and cleanup scripts behave predictably on empty directories and filenames with spaces.
Bash Wildcards — Quick Reference
| Pattern | Matches |
|---|---|
*.log |
Names ending in .log in the current directory |
file?.txt |
file plus exactly one character plus .txt |
file[0-9].txt |
One digit in the bracket position |
[!0-9]* |
Names whose first character is not a digit |
**/*.conf |
Config files in nested directories when globstar is on |
In pathname expansion, globs match existing pathnames rather than arbitrary string content. When nothing matches and nullglob is off, the pattern usually stays literal.
What Bash Globbing Actually Does
When you type rm *.log, Bash does not pass the two-character string *.log to rm unless no file matches. First it scans the current directory, finds every name that fits the pattern, and replaces the pattern with that list of paths. Only then does rm run.
That step is filename expansion, also called globbing or pathname expansion. It runs late in Bash parsing, after brace expansion and parameter expansion but before the command executes. Patterns are not regular expressions: * means any string in one path component, not regex repetition.
To see the result without touching files, wrap each expanded argument in angle brackets:
printf '<%s>\n' *.log<app.log>
<error.log>
<file with spaces.log>Each line is one argument Bash built from the *.log pattern. The command never saw a literal asterisk because matching files existed in the directory.
Build a Safe Demo Directory
Create a disposable lab folder with normal files, hidden names, mixed case, spaces, and a nested config file. Run these commands from a directory where you can add bash-glob-lab without affecting real data.
mkdir -p bash-glob-lab/sub/nestedThat creates the top-level lab directory and one nested path for later globstar tests.
Add the fixture files the examples below expect:
cd bash-glob-lab && touch app.log error.log readme.txt 'file with spaces.log' .hidden.log .env app.conf sub/nested/service.conf sub/nested/readme.txt App.LOG 1.txt 2.txt a9.txttouch prints nothing on success. List what landed in the lab root — hidden dotfiles appear only with -a:
ls -1a.
..
.env
.hidden.log
1.txt
2.txt
a9.txt
app.conf
app.log
App.LOG
error.log
file with spaces.log
readme.txt
subStay in bash-glob-lab for the rest of the walkthrough unless a section says otherwise.
Basic Wildcard Examples
Three pattern forms cover most day-to-day globbing.
| Pattern | Matches |
|---|---|
* |
Any string, including an empty string, in one path component |
? |
Exactly one character in that component |
[...] |
One character from the set, range, or class inside the brackets |
Bracket expressions support more than single-letter lists:
| Pattern | Meaning |
|---|---|
[abc] |
One of a, b, or c |
[0-9] |
One digit |
[!0-9] or [^0-9] |
One character that is not a digit |
[[:digit:]] |
One character in the POSIX digit class |
* in the current directory expands to every visible name (dotfiles are skipped unless dotglob is on):
printf '<%s>\n' *<1.txt>
<2.txt>
<a9.txt>
<app.conf>
<app.log>
<App.LOG>
<error.log>
<file with spaces.log>
<readme.txt>
<sub>? requires a single character before the suffix. No one-character .log files exist here, so the pattern stays literal:
printf '<%s>\n' ?.log<?.log>A letter set such as [ae]*.log picks names that start with a or e:
printf '<%s>\n' [ae]*.log<app.log>
<error.log>A digit range matches numbered text files in the lab:
printf '<%s>\n' [0-9].txt<1.txt>
<2.txt>Negated brackets exclude names whose first character is a digit:
printf '<%s>\n' [!0-9]*.txt<a9.txt>
<readme.txt>1.txt and 2.txt are excluded because their first character is a digit; a9.txt and readme.txt begin with non-digits.
POSIX character classes use the same bracket syntax with a colon-wrapped name:
printf '<%s>\n' [[:digit:]]*.txt<1.txt>
<2.txt>App.LOG does not appear because Bash pathname matching is case-sensitive by default.
What Happens When a Glob Matches Nothing?
With default settings, an unmatched pattern is left unchanged. Bash passes the literal string to the command.
printf '<%s>\n' *.missing<*.missing>That behavior explains a common loop bug. With nullglob off, for still runs once and sets f to the literal text *.missing:
for f in *.missing; do echo "file=$f"; donefile=*.missingA script that checks [[ -f "$f" ]] on that value silently does nothing useful — it never sees zero files. Use nullglob, failglob, or an explicit existence check when empty matches matter.
nullglob vs failglob
Both options change no-match behavior through shopt. They solve different problems.
| Option | When nothing matches |
|---|---|
| Default | Pattern stays literal (*.missing) |
nullglob |
Pattern removed — zero words |
failglob |
Bash aborts the command with an error |
Turn on nullglob so unmatched patterns disappear instead of becoming literal arguments:
shopt -s nullglobCount array elements to confirm zero matches — do not rely on printf alone, because printf '<%s>\n' with no operands still prints an empty angle-bracket line:
files=( *.missing )Ask how many elements landed in the array:
printf 'matches=%d\n' "${#files[@]}"matches=0A for loop over *.missing now runs zero times:
for f in *.missing; do printf '<%s>\n' "$f"; doneThe loop produces no output when nothing matches.
Turn nullglob off when you are done experimenting so later examples match default behavior:
shopt -u nullglobfailglob is stricter: any unmatched pattern is an error. In a subshell with failglob enabled, expanding *.missing fails before printf runs:
bash -c 'shopt -s failglob; printf "<%s>\n" *.missing'bash: line 1: no match: *.missingPick nullglob for loops that should simply skip when no files match. Pick failglob when a missing match means the script must stop immediately.
Hidden Files and dotglob
By default, * and ?.suffix ignore names that start with .. A separate pattern is required to reach them.
List dot-prefixed names with an explicit leading dot in the pattern:
printf '<%s>\n' .*<.env>
<.hidden.log>On Bash 5.2, globskipdots is enabled by default, so . and .. are excluded even from patterns such as .*. Older Bash releases do not provide this option and may handle explicit dot patterns differently.
*.log without dotglob still omits .hidden.log:
printf '<%s>\n' *.log<app.log>
<error.log>
<file with spaces.log>Enable dotglob to include hidden names in other globs:
shopt -s dotglobNow *.log also returns the hidden log file:
printf '<%s>\n' *.log<app.log>
<error.log>
<file with spaces.log>
<.hidden.log>Disable dotglob before moving on unless you want it for the rest of the session:
shopt -u dotglobCase-Insensitive Globbing with nocaseglob
The lab includes both app.log and App.LOG. With default case-sensitive matching, only the lowercase name appears in *.log:
printf '<%s>\n' *.log<app.log>
<error.log>
<file with spaces.log>Enable nocaseglob when alphabetic case should not matter:
shopt -s nocaseglobThe same pattern now includes App.LOG:
printf '<%s>\n' *.log<app.log>
<App.LOG>
<error.log>
<file with spaces.log>Turn nocaseglob off before continuing unless your script needs it:
shopt -u nocaseglobRecursive Matching with globstar
globstar lets a double asterisk span directory levels. globstar requires Bash 4 or later.
Without globstar, a double asterisk has no recursive meaning. It behaves like ordinary * wildcard matching within a pathname component, so a pattern such as **/*.conf may match shallow paths or remain literal when nothing matches.
With globstar off, the pattern stays literal in this lab because no pathname fits:
shopt -u globstarExpand the recursive-looking pattern with globstar disabled:
printf '<%s>\n' **/*.conf<**/*.conf>Enable recursive globbing:
shopt -s globstarThe same pattern now finds config files in the current directory and below:
printf '<%s>\n' **/*.conf<app.conf>
<sub/nested/service.conf>With globstar enabled, a double asterisk can match zero directories, so **/*.log still includes logs in the current directory:
printf '<%s>\n' **/*.log<app.log>
<error.log>
<file with spaces.log>Turn globstar off when finished if you rely on default shell behavior in other scripts:
shopt -u globstarExtended Patterns with extglob
extglob adds extended shell-pattern operators for alternatives, repetition, and negation. Enable it before using the forms below:
| Form | Meaning |
|---|---|
?(pattern) |
Zero or one occurrence of the pattern |
*(pattern) |
Zero or more occurrences |
+(pattern) |
One or more occurrences |
@(a|b) |
Exactly one alternative |
!(pattern) |
Anything except the pattern |
In an interactive shell with history expansion enabled, ! may be interpreted as history syntax. If that interferes with !(...), disable history expansion for the session with set +H. Shell scripts normally do not have history expansion enabled.
Enable extended globs for the current shell:
shopt -s extglobMatch everything in the current directory that is not a .log file:
printf '<%s>\n' !(*.log)<1.txt>
<2.txt>
<a9.txt>
<app.conf>
<App.LOG>
<readme.txt>
<sub>App.LOG remains because *.log is case-sensitive unless nocaseglob is on. Combine extensions with alternation:
printf '<%s>\n' @(*.log|*.conf)<app.conf>
<app.log>
<error.log>
<file with spaces.log>In scripts, enable extglob before defining functions or other compound constructs that contain extended glob patterns, because Bash may need the option enabled while parsing them.
Disable extglob after the demo unless your script sets it at the top:
shopt -u extglobQuoting and Globbing
Quotes disable pathname expansion. The characters inside stay exactly as typed.
Single-quoted patterns never glob:
printf '<%s>\n' '*.log'<*.log>Double quotes still block globs while allowing parameter expansion. For how quoting fits with variables and word splitting, see Bash single vs double quotes.
Store a pattern in a variable when you need to choose it at runtime. Unquoted expansion still globs:
pattern='*.log'Unquoted $pattern is expanded and then globbed like a literal *.log on the command line:
printf '<%s>\n' $pattern<app.log>
<error.log>
<file with spaces.log>Quoted expansion keeps the pattern literal:
printf '<%s>\n' "$pattern"<*.log>Unquoted $pattern can undergo word splitting and pathname expansion, so use this technique only when the pattern itself is controlled. For fixed file patterns in scripts, direct glob syntax and arrays such as files=( *.log ) are easier to reason about. Avoid eval for pathname expansion.
Globs in Arrays and Loops
Assigning a glob to an array captures each match as one element. Bash variables covers array assignment and ${files[@]} when you need one pathname per element instead of a space-delimited string.
files=( *.log )This works as expected here because .log files exist. If matches may be absent, enable nullglob before assigning the array; otherwise the unmatched pattern becomes one literal array element and ${#files[@]} reports 1 instead of 0.
Print each array element on its own line:
printf '<%s>\n' "${files[@]}"<app.log>
<error.log>
<file with spaces.log>The element count is available with ${#files[@]}:
echo "count=${#files[@]}"count=3In a Bash for loop, enable nullglob when an empty match should mean zero iterations:
shopt -s nullglobQuote the loop variable so filenames with spaces stay one word:
for f in *.log; do printf 'file=[%s]\n' "$f"; donefile=[app.log]
file=[error.log]
file=[file with spaces.log]An alternative when you do not want to change shopt settings is an existence guard:
shopt -u nullglobSkip entries that are not real files — check if file exists in Bash documents -e, -f, and related tests when a glob might stay literal:
for f in *.log; do [[ -e "$f" ]] || continue; printf 'file=[%s]\n' "$f"; doneThe guard skips the literal *.log string when no files match. Dropping quotes around $f inside the loop body reintroduces word splitting — always use "$f" when passing the value to rm, cat, or mv.
For optional file sets, build an array and check the count before acting:
shopt -s nullglobCollect matches into an array:
files=( *.log )Remove them only when the array is non-empty — branch with Bash if else before destructive commands:
if ((${#files[@]})); then printf 'removing: %s\n' "${files[@]}"; rm -- "${files[@]}"; firemoving: app.log
removing: error.log
removing: file with spaces.logrm -- marks the end of options so a filename starting with - is not treated as a flag. It does not stop a real file literally named *.log from being removed when the glob matches that name.
Turn nullglob off afterward if you need default literal behavior again:
shopt -u nullglobGlob vs Regex vs Brace Expansion
These three mechanisms are easy to mix up because they all use punctuation in the shell, but they run at different stages and follow different rules.
| Mechanism | When it runs | Typical syntax | Purpose |
|---|---|---|---|
| Brace expansion | Early | {a,b}.txt |
Build fixed lists of strings |
| Globbing | Late | *.log, nested conf patterns with globstar |
Match existing pathnames |
| Regex | Inside [[ string =~ regex ]] or tools like grep |
^[0-9]+$ |
Pattern match on string content |
Brace expansion does not look at the filesystem. It only copies text:
printf '<%s>\n' {a,b}.txt<a.txt>
<b.txt>Those names were generated even though a.txt and b.txt do not exist in the lab. Globbing, by contrast, only produces names that are already on disk (or leaves the pattern literal when nothing matches).
Regex belongs in Bash regex with [[ "$line" =~ pattern ]] or external commands — not in unquoted filename patterns. The Bash case statement uses shell patterns (similar to globs), not full regex, unless you enable nocasematch or use extended syntax deliberately.
Common Dangerous Globbing Patterns
A few habits cause data loss or silent wrong behavior.
| Pattern | Risk |
|---|---|
rm -rf * |
Recursively removes all visible matches without prompting; hidden dotfiles normally remain |
rm $file with unquoted variable holding multiple paths |
Word splitting can remove unintended names |
chmod 777 * |
Broadly changes permissions on every visible match and skips hidden names |
Looping on *.log without nullglob when logs may be absent |
Loop runs once with literal *.log |
for f in $(ls *.txt) |
Parses ls output; breaks on spaces and metacharacters |
For destructive work, print matches first with printf '%s\n' *.log, or use a nullglob array and check ${#files[@]} before rm.
Summary
Bash globbing expands wildcard patterns into real pathnames before your command runs. You used printf '<%s>\n' pattern to see each resulting argument: *, ?, and bracket expressions for everyday matches, literal patterns when nothing fits, and shopt options when defaults are wrong for your script.
nullglob removes unmatched patterns so loops and arrays see zero items; failglob turns a non-match into a hard error. dotglob and nocaseglob change which hidden and mixed-case names participate. globstar enables recursive directory matching, and extglob adds negation and alternation. Quoting suppresses expansion — unquoted variables glob, quoted ones do not — and arrays plus "$f" in loops keep spaced filenames intact.
Globbing is not regex and not brace expansion. In scripts that depend on particular no-match, hidden-file, or recursive behavior, set the required shopt options explicitly rather than relying on the user's interactive shell configuration.
References
- GNU Bash manual — Filename Expansion
- GNU Bash manual — Pattern Matching
- GNU Bash manual — The Shopt Builtin
- GNU Bash manual — Brace Expansion
Frequently Asked Questions
Why does *.log remain literal?
Bash leaves the pattern unchanged when no file matches and nullglob is off. The command receives the characters *.log, not an empty list. Enable nullglob, use failglob to abort, or test with compgen -G '*.log' before acting.
Is * regex?
No. In globs, * matches any string within one path component. Regex * repeats the previous atom and applies in [[ string =~ regex ]] or tools like grep, not during pathname expansion.
How do I recursively match files?
Enable shopt -s globstar, then use recursive path patterns (two asterisks, a slash, then the filename pattern). Without globstar, a double asterisk has no recursive meaning and may match only within one pathname component or stay literal when nothing fits.
Does * match hidden files?
Not by default. Names starting with . are skipped unless you enable dotglob or match them with a leading dot in the pattern, such as .*.
nullglob vs failglob?
nullglob drops unmatched patterns so you get zero words. failglob treats any unmatched pattern as a fatal error and stops the command. Use nullglob for optional file sets; use failglob when a missing match means the script must not continue.

