SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor

Bash Globbing: Wildcards, nullglob and extglob

Learn how Bash turns wildcards into filenames before commands run, plus the exact roles of nullglob, dotglob, globstar and extglob negation.

Wetin bash globbing dey do before your command run

Bash globbing na when shell turn pattern like *.log into sorted list of real filenames before command start. rm *.log no dey give rm any pattern. Bash go read directory, keep names wey match, sort dem, then run rm with each name as separate argument. rm program no fit know say pattern ever dey involved.

Almost everything wey dey surprise person about globbing come from this one fact. Shell dey do the matching against files wey exist at that time, and command dey see only the final list. The formal name for this na pathname expansion. Globbing na the everyday name, and both mean the same thing.

Build one small tree to practise

Every example below dey use the same files. Create dem for one temporary directory, so nothing wey matter to you dey nearby and cleanup na one command.

lab=$(mktemp -d)
cd "$lab"
mkdir -p logs archive/2025 archive/2026
touch app.log app.log.1 app.log.2 app.log.10 error.log debug.LOG
touch notes.txt notes.md README 'weekly report.log'
touch .env .apprc
touch logs/nginx.log logs/nginx.log.1
touch archive/2025/dec.log archive/2026/jan.log

mktemp -d dey create fresh directory and return the path, while $(...) dey capture that path inside variable lab. If this syntax dey new to you, how command substitution dey capture command output explain everything well.

Check wetin you build, and check your shell too, because one note for the end depend on the version:

ls -a
bash --version

Everything here dey work for bash 4.0 and later. The globskipdots note near the end apply to bash 5.2 and later, wey current Ubuntu and Debian releases dey ship as of August 2026.

The preview command wey dem use throughout na printf '%s\n' PATTERN. E dey print one match for each line and e no change anything for disk, so e safe to run am against any pattern wey you never sure about.

Three wildcard: star, question mark, and brackets

  • * dey match any run of characters, including empty one.
  • ? dey match exactly one character.
  • [...] dey match exactly one character wey dey inside the set for the brackets.
printf '%s\n' *.log
printf '%s\n' app.log.?
printf '%s\n' app.log.[0-9]
printf '%s\n' notes.*
printf '%s\n' *

Compare the second line with the file list wey you create. ? mean one character and nothing more, so suffix wey get two digits no fit match that pattern. Make am wider to app.log.[0-9]* and run am again to see the difference.

Two rules apply to all three wildcards. No wildcard dey ever match /, so pattern dey stay inside one directory level. And you must type leading . literally, so pattern wey start with * go skip every hidden name. Na that second rule make rm * leave your dotfiles alone.

Glob results dey come sorted according to collation order for the current locale. This na character-by-character text sort. So suffix of 10 dey sort before suffix of 2, because comparison no dey read either one as number. Run printf '%s\n' app.log.*, then ls -v app.log.*, and compare the two: ls -v dey sort embedded numbers by value, while sort -V dey do the same work for a pipeline.

Character classes and ranges

For inside the brackets, you fit write list, range, or negation.

  • [ch] dey match one character, either c or h.
  • [a-f] dey match one character for the range a to f.
  • [!0-9] dey match one character wey no be digit. [^0-9] mean the same thing.
printf '%s\n' [dn]*
printf '%s\n' *.[A-Z]*
printf '%s\n' [A-Z]*

Ranges na the part wey fit behave different between machines. The current locale collation order dey resolve range, instead of plain ASCII. So for some systems, [a-z] fit also match uppercase letters. Bash get globasciiranges option wey dey force plain ASCII order. Recent builds enable am by default, so check your own with shopt globasciiranges instead of assuming. If script must match the same way everywhere, e fit set LC_ALL=C by itself on one line near the top. This puts every range back to plain byte order.

That assignment must be a command on its own. If you write am as prefix, LC_ALL=C ls [a-z]* go set the locale for ls only. The shell don already expand the pattern with its own locale before ls start. Na the same rule from the first section show again.

Brace expansion no be globbing

Braces look like wildcard, but dem no dey behave like wildcard at all. Brace expansion dey happen much earlier than globbing, and e no dey check disk.

echo file{1,2,3}.txt
echo {01..10}
echo {a..e}
echo {0..20..5}

None of those files dey exist, but expansion still happen. Na that be the main difference: glob dey ask filesystem question, while brace only dey generate text. Na why braces dey useful for creating things instead of finding dem.

mkdir -p site/{css,js,img}
cp app.log{,.bak}
touch report-{2025,2026}-{01,02}.csv

cp app.log{,.bak} expand into two words, app.log and app.log.bak, because the empty item before the comma reproduce the original text. {01..10} keep its zero padding, because leading zero for either endpoint make bash pad every generated number to the same width.

Expansion order still explain one common failure. Bash expand braces first, then variables, and pathname expansion come last.

n=5
echo {1..$n}

Brace expansion don finish before $n turn into number, so no range ever dey build. Use seq 1 "$n", or use C-style loop written as for ((i=1; i<=n; i++)).

Wetin dey happen when pattern no match anything

Na this be the first way things fit fail, and everybody go meet am at least once. By default, bash dey leave pattern wey no match completely as e be. The word go reach command exactly as you type am, so command go receive the characters *, ., t, m and p and try treat dem as filename.

printf '%s\n' *.tmp

This one no cause wahala with printf. But e fit cause wahala with command wey dey create things. touch *.tmp for directory wey no get any .tmp files go create file wey name na exactly *.tmp, and to delete am later, you need quote am so shell no expand am again: rm -- '*.tmp'.

Two shell options dey change this rule. Test each one with pattern wey no match anything, then unset am.

shopt -s nullglob
printf '%s\n' *.tmp
shopt -u nullglob

shopt -s failglob
printf '%s\n' *.tmp
shopt -u failglob

nullglob go make pattern wey no match expand to nothing, so command go run with arguments wey less pass wetin you write. Na this you want when you dey use loop. When nullglob dey off, for f in *.tmp go run the body once, with f holding the literal pattern. For almost every script wey do this, na bug. When nullglob dey on, the body go run zero times.

nullglob get another danger, because command wey all e arguments don disappear still go run. ls *.nope go turn to bare ls, wey go list the whole directory. grep -l needle *.nope go turn to grep -l needle, wey no get file to read, so e go wait for standard input and look like say e freeze. Turn nullglob on around the loop wey need am, then turn am off again afterwards.

failglob take the other approach: when pattern no match anything, e go be error, bash go report am, and command no go run at all. This setting good for interactive shell, because e go stop mistyped pattern before dem hand am to rm as literal name.

Dotglob: hidden files

shopt -s dotglob
printf '%s\n' *
shopt -u dotglob

Run that block, then compare the listing with the plain printf '%s\n' * we see earlier. dotglob dey remove the rule about leading dot, so * dey match hidden names too. The entries . and .. always remain excluded under dotglob.

Without dotglob, .* na the usual way to reach hidden files, but before e dey dangerous. For bash before 5.2, .* still dey match . and .., so recursive command like chmod -R 755 .* fit enter the parent directory directly. Bash 5.2 add globskipdots option, and e dey enabled by default. This option keeps . and .. out of every expansion. Run shopt globskipdots before you depend on am, because older box no go get am.

globstar: match go everywhere for tree

shopt -s globstar
printf '%s\n' **/*.log
printf '%s\n' **/
shopt -u globstar

When globstar dey set, a ** wey form complete path component fit match files and directories for any depth, including zero level down. So **/*.log cover current directory, plus logs/ and everything wey dey under archive/. A **/ wey get trailing slash dey match directories only. This make am quick way to see the shape of a tree.

If globstar no dey set, ** dey behave like ordinary * and e go remain for one directory. Run the same pattern with the option off and compare the result. If recursive pattern wey you copy from somewhere else return far less than you expect, na usually this be the cause. globstar dey off by default for every bash.

One limit wey you need know: ** no dey follow symbolic links to directories. If symlinks join the tree together, use find -L instead.

nocaseglob, for names wey case no match

shopt -s nocaseglob
printf '%s\n' *.log
shopt -u nocaseglob

nocaseglob dey make filename matching ignore case, so uppercase .LOG suffix go match lowercase .log pattern. Compare this listing with the one from the first section. Turn the option off again immediately, because if e remain on, e go change matching for every command wey follow for that shell, and that one fit hard to debug one hour later.

nocaseglob dey apply only to filename expansion. The related option for pattern matching inside a case statement or a double bracket test na nocasematch, and you must set the two separately.

extglob: patterns wey fit talk say “no be this”

Extended patterns dey off by default. Turn dem on for dia own line:

shopt -s extglob

There be five forms, and each one dey take list of patterns wey | separate:

  • ?(list) dey match zero or one occurrence.
  • *(list) dey match zero or more.
  • +(list) dey match one or more.
  • @(list) dey match exactly one of the alternatives.
  • !(list) dey match anything wey no match any alternative.

Shell wey already get the option on must read extended pattern, so the batch below dey inside small script wey bash -O extglob start. The -O flag set one shopt option before the new shell read one line from the file. This make every pattern parse correctly. The last paragraph for this section explain why the order matter.

cat > patterns.sh <<'EOF'
printf '%s\n' @(app|error).log
printf '%s\n' app.log.+([0-9])
printf '%s\n' *.@(md|txt)
printf '%s\n' !(*.log)
printf '%s\n' !(*.log|*.md|README)
EOF
bash -O extglob patterns.sh

+([0-9]) mean one or more digits, so e cover numbered suffix wey fit get any length. Na this fix the ? limit from before, and e good make you run both patterns one after another so you fit see the difference.

The negation form answer “everything except”, and na why you go enable extglob at all. Two things about am dey surprise people.

E dey match directories as well as files, so ls !(*.log) list the contents of any directory for the result instead of the directory name. Use ls -d, or the printf preview, to see the names themselves.

E still follow the dot rule like every other glob, so !(*.log) cover every visible name wey no end with .log. Hidden names need dotglob set too.

The ! for !(...) na glob negation. E no get connection with history expansion and the bang character, wey be separate feature and dey run for different stage.

One more trap dey, and this one happen for parse time. Bash parse complete command before e run any part of am, so enabling extglob and using extended pattern for the same line go fail: the pattern go parse while the option still dey off. Keep shopt -s extglob for its own line, before the patterns, and near the top of the script. Bash parse function body when e define the function, so the option must dey on before the definition, no be before the call. Starting the shell with bash -O extglob, the way the batch above do am, settle the order before any parsing start.

Shell na e dey expand the pattern, command no dey do am

Na the second failure mode be this, and e explain one whole group of bugs.

grep -l needle *.log
find . -name '*.log'
find . -name *.log

For the first line, bash dey do the matching, then grep receive list of filenames. The second line quote the pattern, so find receive the five characters *.log and do the matching by itself, for every depth below the starting point. The third line na the bug: bash first expand the pattern against current directory only, so find receive instruction to look for one specific name. For directory wey get one match, e silently search for the wrong thing. If matches dey two or more, find report usage error, because the extra names enter the place wey e expect expression.

The rule short. If na the command suppose handle the pattern, quote am. If na the shell suppose handle am, leave am bare. The same split apply to --exclude patterns for tar and rsync, and the --include filters for grep.

Globs no be regular expressions too, even though dem share some characters. For glob, * mean any run of characters. For regular expression, * mean zero or more of the thing wey come before am, so grep '*.log' dey ask for something quite different from how e look.

Variables follow the same order of operations. Globbing happen after variable expansion, so if variable hold pattern and you no quote am, e go expand against the directory.

pat='*.log'
printf '%s\n' $pat
printf '%s\n' "$pat"

If you no quote am, bash go glob the variable result. If you quote am, the pattern remain literal. Another related result wey you suppose depend on be say: words wey globbing produce no dey split again by spaces. So for f in *.log handle filename wey get space as one name, while for f in $(ls *.log) break that name into two words. Loop over the glob directly.

Preview destructive pattern wey you wan run first

Make you never use rm as the first command to test new pattern. Run the pattern with something harmless, read the list, then change only the command for the front of the line.

target='!(*.log)'
printf '%s\n' $target
ls -ld -- $target
rm -- $target

Writing the pattern inside target one time stop the three lines from become different, and $target dey without quotes on purpose because na that one let bash expand am. printf '%s\n' dey print one name for each line and e no touch anything. ls -ld dey show each entry by itself instead of listing the contents of directories, and the mode column dey mark which entries be directories. Na this thing you need check before any recursive delete. How to read drwxr-xr-x permission bits cover that column.

Use up arrow bring back the previous line, then edit only the command, so the pattern remain exactly the same. If you type pattern again from memory, na there mistake fit enter.

Put -- before the pattern. E mark where options end, so filename wey start with - go count as name instead of flag.

If you want globbing make e stop for part of script, set -f go disable am and set +f go enable am again.

This habit important because rm no get undo. How to recover files deleted with rm -rf slow and usually no complete, so preview dey cost far less than the other option.

Clean the practice directory after you finish. Print the path and read am first, because rm -rf wey point to wrong variable na exactly the kind accident this section dey talk about.

cd ~
echo "$lab"
rm -rf -- "$lab"

FAQ

Why find . -name *.log dey find wrong files?

Because bash dey expand *.log before find even start. The shell dey match the pattern against current directory, then e pass the filename wey result to find. find come search for that one name at every depth. If two or more files match, find go receive extra arguments where e expect expression, then e report usage error. Quote the pattern as find . -name '*.log' so find go do the matching by itself, with its own rules and recursion.

Wetin e mean when my pattern dey show as literal text?

E mean say nothing match. By default, bash dey leave pattern wey no match untouched and pass the raw characters to the command. The command then treat dem as filename. Set shopt -s failglob to turn unmatched pattern into error wey go stop the command, or shopt -s nullglob to make am expand to nothing. Under nullglob, check say the command still make sense when e get no arguments at all, because ls *.nope go become bare ls and list the whole directory.

How I fit match every file except one pattern for bash?

Enable extended patterns with shopt -s extglob for line wey stand alone, then use the negation form. !(*.log) dey match names wey no end with .log, and !(*.log|*.md) dey exclude both. Hidden files no dey enter the result unless dotglob set as well, and directories dey included. So preview the pattern with printf '%s\n' !(*.log) before you give am to rm.

Why * dey skip hidden files?

A leading dot must match literally, so * no dey ever match name wey start with one. Use .* to reach hidden names, or set shopt -s dotglob to include dem inside every ordinary pattern. The entries . and .. always dey excluded under dotglob, and bash 5.2 and later keep dem out of .* too through the globskipdots option, wey dey on by default.

I need globstar before ** go work?

Yes. Without shopt -s globstar, bash dey treat ** as ordinary *. E dey match inside one directory and stop there. When globstar dey on, ** wey form complete path component fit match for any depth. So **/*.log go reach current directory and every subdirectory under am, while **/ by itself dey match directories only. Note say ** no dey follow symbolic links to directories, so use find -L for tree wey symbolic links build.

#bash#globbing#shell#linux#scripting