Going Wild with Asterisks and Question Marks: Complete Linux Wildcard and Globbing Pattern Guide

Going wild with asterisks and questions marks

I still remember the first time globbing bit me for real: I ran rm file?.txt expecting it to delete file1.txt through file9.txt, and it deleted exactly one file — file1.txt — because ? matches exactly one character, not a range. Small misunderstanding, no real damage that day, but it’s the kind of thing that could have gone badly on a production box. Wildcards feel simple on the surface, and mostly they are, but there’s real nuance underneath, and knowing exactly how the shell expands these patterns before a command ever runs is the difference between confident scripting and nervously typing ls first to double-check every time.

What Globbing Actually Is

“Globbing” is the shell’s own filename-expansion mechanism — before your command even runs, the shell scans the current directory (or wherever the pattern points) for filenames matching a pattern and substitutes the literal list of matches in place of the pattern. This is crucial to understand: the command you’re running (ls, rm, cp, whatever) never sees the wildcard at all. It only ever sees the expanded list of real filenames. Globbing is entirely the shell’s job, done before execution.

I can demonstrate this distinction directly. Set up some test files:

$ touch file1.txt file2.txt file10.txt fileA.txt image.png image.jpg doc.pdf
$ echo *.txt
file1.txt file2.txt file10.txt fileA.txt

echo never sees *.txt — bash expanded it into the literal filename list before calling echo. This is why globbing works identically no matter which command you put in front of it.

The Core Wildcard Characters

* — Match Any Number of Characters (Including None)

$ echo *.txt
file1.txt file2.txt file10.txt fileA.txt

* matches zero or more of any character, so *.txt matches everything ending in .txt regardless of what comes before it, including nothing at all (a bare .txt would also match if it existed).

? — Match Exactly One Character

$ echo file?.txt
file1.txt file2.txt fileA.txt

Notice file10.txt is not in that list — ? matches exactly one character, and 10 is two characters. This is precisely the mistake I described in the intro, and it’s the single most common wildcard misunderstanding I see.

[...] — Match Any One Character From a Set

$ echo file[12].txt
file1.txt file2.txt

This matches file1.txt or file2.txt — one character from inside the brackets, in that exact position. You can also specify ranges:

$ echo file[0-9].txt
file1.txt file2.txt

[!...] or [^...] — Negated Character Class

$ echo file[!1].txt
file2.txt fileA.txt

This matches any single character in that position except 1. Both ! and ^ work for negation in bash, though ! is the more traditionally portable form across shells.

Brace Expansion — Not Technically Globbing, But Related

Brace expansion ({...}) is a distinct bash feature, not POSIX globbing, but it’s used constantly alongside wildcards and deserves coverage here. Unlike */?/[...], brace expansion doesn’t check the filesystem at all — it’s pure text expansion that happens whether or not matching files exist.

$ echo file{1,2,10}.txt
file1.txt file2.txt file10.txt

This is genuinely useful for generating exact filename lists you already know, rather than pattern-matching against what exists on disk. A classic use is creating multiple directories at once:

$ mkdir -p project/{src,docs,tests,build}

Brace expansion also supports ranges:

$ echo file{1..5}.txt
file1.txt file2.txt file3.txt file4.txt file5.txt

$ echo {a..e}
a b c d e

Important distinction: because brace expansion doesn’t check the filesystem, echo file{99,100}.txt will happily print both names even if neither file exists. Globbing (*, ?, [...]) only ever expands to files that actually exist — if nothing matches, the pattern is either left as literal text (bash default) or causes an error (if nullglob/failglob is set, covered below).

Hidden Files Need an Explicit Dot

This one surprises people coming from GUI file managers, where “show hidden files” is just a toggle. In globbing, a leading * never matches a leading dot:

$ touch .hidden
$ echo *
file1.txt file2.txt file10.txt fileA.txt image.png image.jpg doc.pdf

.hidden is missing from that list entirely, even though * should in theory match “everything.” To include hidden files, you must start the pattern with an explicit dot:

$ echo .*
. .. .hidden

Note this also catches . and .. (the current and parent directory entries), which is exactly why rm .* is a genuinely dangerous command — a classic disaster pattern that has deleted people’s parent directories. If you need hidden files specifically, excluding . and .., a safer pattern is:

$ echo .[!.]*
.hidden

Recursive Globbing with ** (globstar)

By default, ** behaves exactly like a single * in bash — it does not recurse into subdirectories. Recursive double-star matching requires explicitly enabling the globstar shell option:

$ shopt -s globstar
$ mkdir -p a/b/c && touch a/b/c/deep.txt
$ echo **/*.txt
a/b/c/deep.txt file1.txt file10.txt file2.txt fileA.txt

I verified this directly — without globstar enabled, that same pattern only matches files in the current directory, silently missing anything nested deeper. This shell option is off by default precisely because unlimited recursive expansion across a large directory tree can be slow and occasionally hazardous with destructive commands, so bash makes you opt in deliberately.

To turn it back off:

$ shopt -u globstar

Extended Globbing with extglob

Bash has a more powerful pattern-matching mode you can enable explicitly, giving you regex-like alternation and negation directly in glob patterns:

$ shopt -s extglob
$ echo !(file1|file2).txt

I tested this against my sample files and it correctly excluded file1.txt and file2.txt, matching everything else ending in .txt. The extglob operators are:

PatternMeaning
?(pattern-list)Zero or one occurrence of the given patterns
*(pattern-list)Zero or more occurrences
+(pattern-list)One or more occurrences
@(pattern-list)Exactly one occurrence
!(pattern-list)Anything except the given patterns

This is genuinely powerful for precise matching and worth enabling when standard globs aren’t expressive enough — it’s a step up in capability without going all the way to a full regex engine.

$ shopt -u extglob

Character Classes (POSIX-Style)

Beyond simple ranges like [0-9], bash supports named POSIX character classes inside brackets, which are more portable and readable than manually spelling out ranges:

ClassMatches
[[:alpha:]]Any alphabetic character
[[:digit:]]Any digit
[[:alnum:]]Any letter or digit
[[:upper:]]Any uppercase letter
[[:lower:]]Any lowercase letter
[[:space:]]Whitespace characters
[[:punct:]]Punctuation characters
$ echo file[[:digit:]].txt
file1.txt file2.txt

Handling No Matches: nullglob and failglob

By default, if a glob pattern matches nothing, bash leaves the pattern as literal text rather than removing it or erroring — this trips up a lot of scripts:

$ echo *.nonexistent
*.nonexistent

That’s the literal string being printed, not an error and not an empty result. In a script, this can cause a command to receive a garbage literal filename it was never meant to see. Two shell options change this behavior:

$ shopt -s nullglob   # non-matching patterns expand to nothing at all
$ echo *.nonexistent
                        # (prints nothing)

$ shopt -s failglob    # non-matching patterns cause an error instead
$ echo *.nonexistent
bash: no match: *.nonexistent

For robust scripting, I set nullglob deliberately at the top of scripts that loop over glob results, specifically to avoid the classic bug where a for-loop over an empty glob silently processes the literal unmatched pattern string as if it were a real filename:

#!/bin/bash
shopt -s nullglob
for f in /var/log/app/*.log; do
    echo "Processing: $f"
done

Without nullglob, if no .log files exist, that loop body still runs once with f set to the literal string /var/log/app/*.log — a real and common source of script bugs.

Wildcards vs. Regular Expressions — Don’t Confuse Them

This is a persistent point of confusion for people newer to Linux: shell globbing and regular expressions (used by grep, sed, awk) look superficially similar but follow completely different rules, and mixing up their meanings causes real bugs.

SymbolMeaning in a glob patternMeaning in a regex
*Zero or more of any characterZero or more of the preceding character
.A literal dotAny single character
?Exactly one of any characterZero or one of the preceding character
[...]Character set (same concept in both, actually)Character set (same concept)

The * difference is the one that catches people most often: in a glob, * alone means “match anything.” In regex, * means “repeat the previous token,” so a bare * at the start of a regex is often either an error or matches literally nothing useful — you’d need .* in regex to get the glob’s “match anything” behavior.

Quoting to Prevent Unwanted Expansion

Sometimes you want a literal * or ? in a string, not glob expansion. Quoting (single or double quotes) or backslash-escaping prevents the shell from treating the character as a wildcard:

$ echo "Cost is *not* fixed"
Cost is *not* fixed

$ echo Cost is \*not\* fixed
Cost is *not* fixed

This matters a lot when constructing search patterns for other tools. If I’m passing a literal asterisk to grep, I need to make sure bash doesn’t try to glob-expand it first against files in the current directory:

$ grep '3\*4' calculations.txt   # searching for literal text "3*4"

Without quoting, if any filename in the current directory happened to match a pattern containing that unquoted text, bash could substitute filenames in before grep ever runs — a subtle and occasionally very confusing bug.

Practical Sysadmin Use Cases

Bulk-renaming or moving files by extension:

$ mv *.jpg photos/

Cleaning up old log files matching a naming convention:

$ rm app-2024-*.log

Backing up specific numbered configuration versions:

$ cp config[1-3].conf /backup/

Selectively archiving everything except certain files (with extglob):

$ shopt -s extglob
$ tar czf backup.tar.gz !(*.tmp|*.log)

Finding files across nested directories (with globstar):

$ shopt -s globstar
$ ls **/*.conf

Security Implications

Globbing has a genuinely important security dimension, particularly around argument injection. If an attacker can create a file with a carefully crafted name in a directory you’re about to glob over, they can potentially inject option flags into the command that processes the expansion. The classic textbook example: a file literally named -rf sitting in a directory where someone later runs rm *.

$ touch -- -rf
$ ls
-rf
$ rm *
# expands to: rm -rf     <- this could be catastrophic if other files exist too

Mitigations I actually use:

  • Prefix glob-expanded arguments with ./ when passing them to commands that interpret leading dashes as options: rm ./* instead of rm *.
  • Use -- to explicitly terminate option parsing before filename arguments: rm -- *.
  • Be especially careful with globbing in scripts that operate on directories where untrusted users can create files — uploaded content directories, shared temp directories, and the like.

Compatibility Across Shells

The core wildcard characters (*, ?, [...]) are POSIX-standard and behave identically across bash, dash, zsh, and ksh. The extensions differ:

  • Brace expansion ({a,b,c}, {1..5}) is a bash/zsh/ksh feature, not POSIX — it will not work in a strict /bin/sh (dash) script.
  • globstar (** recursion) is a bash 4+ feature (also supported differently in zsh, where it’s on by default rather than opt-in).
  • extglob is a bash-specific option; zsh has its own extended globbing syntax that’s more powerful still but not identical in syntax.

If you’re writing portable scripts meant to run under /bin/sh on Debian-based systems (which points to dash, not bash), stick to plain POSIX globbing (*, ?, [...]) and avoid brace expansion, globstar, and extglob entirely — they will either fail outright or silently behave as literal text rather than expanding as expected.

Troubleshooting

A glob pattern isn’t matching files I expect it to: Check for a leading dot on hidden files (needs an explicit . in the pattern), check whether ? vs * is appropriate for the character count, and confirm globstar/extglob are enabled if the pattern relies on them.

A command receives a literal, unexpanded pattern string as an argument: Almost always means no files matched and nullglob wasn’t set, so bash left the pattern as literal text rather than removing it. Enable nullglob in scripts that need to handle “zero matches” gracefully.

Wildcard behaves differently in a script than it did when I typed it manually: Check the script’s shebang line — #!/bin/sh on Debian/Ubuntu points to dash, not bash, and dash doesn’t support brace expansion, globstar, or extglob at all.

Summary

Globbing is entirely a shell-side text expansion that happens before any command runs — the command itself only ever sees the final, expanded list of real filenames. * matches any number of characters, ? matches exactly one, [...] matches a single character from a set (with [!...] for negation), hidden files need an explicit leading dot, and more advanced patterns (globstar for recursion, extglob for regex-like alternation) require explicitly opting in via shopt. Understanding the difference between globbing and regular expressions — and being deliberate about quoting, nullglob, and argument-injection risks — is what separates confident, safe wildcard usage from the kind of surprise that deletes the wrong file.

References

  • GNU Bash Reference Manual, “Filename Expansion” — https://www.gnu.org/software/bash/manual/bash.html#Filename-Expansion
  • GNU Bash Reference Manual, “The Shopt Builtin” — https://www.gnu.org/software/bash/manual/bash.html#The-Shopt-Builtin
  • POSIX Shell Command Language specification (pattern matching notation) — https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
Total
1
Shares

Leave a Reply

Previous Post
grep command and its perimeter

grep Command in Linux: Complete Guide to Pattern Matching, Text Searching, and Parameters

Next Post
apropos command in linux and it perimeters

apropos Command in Linux: Complete Guide to Searching Manual Page Names, Descriptions, and Parameters

Related Posts