Globbing and Wildcards
Reviewed & published by Brayan K
By the end of this lesson you'll be able to match many files at once with the shell's wildcards — *, ?, and bracket sets — generate names with brace expansion, recurse with **, expand ~ to your home directory, and know exactly when to quote a pattern so the shell leaves it alone.
Part of the free Command Line course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Match any run of characters with * (and why it skips dotfiles)
- Match a single character with ? and build bracket sets [abc] / [a-z]
- Negate a set with [!...] or [^...]
- Generate strings with brace expansion {a,b} and {1..10}
- Recurse with ** (shopt -s globstar) and expand ~ to your home
- Tell globbing from regex, and quote patterns to disable expansion
1. Set Up a Predictable Sandbox
Globbing matches the files that actually exist, so to get the same output every time we first create a known set of files. Run this once; every later example assumes you're inside this globdemo folder.
# Make a clean sandbox and create predictable sample files so every
# example below produces the SAME output on any machine.
mkdir -p globdemo && cd globdemo
# A spread of names: numbered text files, an image, a markdown note,
# and a hidden dotfile.
touch file1.txt file2.txt file10.txt image.png notes.md .hidden
# A nested folder so we can show recursive matching later.
mkdir -p sub
touch sub/deep.txt
# List everything (the -a flag also shows the dotfile):
ls -a2. The * Wildcard: Any Run of Characters
* matches any sequence of characters (including an empty one) within a single filename. Two rules trip people up: a bare * does not match files that begin with a dot (so .hidden is skipped), and * does not cross the / between directories — it stays inside one folder.
# * matches ANY run of characters (including none) within ONE name.
# Match every .txt file in this folder:
echo *.txt
# * by itself matches every NON-hidden entry. Note .hidden is skipped:
echo *
# Dotfiles are excluded unless YOU write the leading dot:
echo .* # now the hidden files show up
# * does NOT cross the / between directories — it stays in one folder:
echo *.png3. The ? Wildcard: Exactly One Character
? matches exactly one character — no more, no less. That's the difference between file?.txt (matches file1.txt) and file??.txt (matches file10.txt). Use one ? per character you want to leave flexible.
# ? matches EXACTLY ONE character — no more, no less.
# file?.txt matches a name with a single char where the ? is:
echo file?.txt # file1.txt and file2.txt, but NOT file10.txt
# file10.txt has two chars there, so it needs two ? marks:
echo file??.txt
# Combine ? with the rest of the name as needed:
echo image.??? # the three-letter extension png4. Bracket Sets [abc] and Ranges [a-z]
A bracket expression [...] matches exactly one character from the listed set. List choices explicitly with [12], or give a range with a hyphen like [1-5] or [a-z]. As always, a glob only lists files that truly exist, so a range that matches nothing is passed through literally.
# A bracket expression [...] matches ONE character from the set.
# List by explicit choices — only file1 and file2 (not file10):
echo file[12].txt
# A RANGE with a hyphen: any single char from 1 through 5:
echo file[1-5].txt # still only file1.txt and file2.txt exist
# Ranges work on letters too. Names starting with a letter in i..z:
echo [i-z]* # image.png and notes.md
# A range that matches nothing here just passes through literally:
echo file[3-9].txt5. Negating a Set with [!...]
Put ! (or ^) as the first character inside the brackets to match a single character that is not in the set. So [!1] matches one character that isn't 1, and [!0-9] matches one non-digit character. Both ! and ^ work in bash.
# Put ! (or ^) FIRST inside the brackets to NEGATE the set:
# match names whose 5th char (after 'file') is NOT a 1:
echo file[!1].txt # file2.txt only — file1.txt and file10.txt excluded
# [!0-9] means 'one character that is not a digit'. Here we ask for a
# name that is a letter, then anything, ending in .png:
echo [!0-9]*.png # image.png starts with a letter
# ^ works the same as ! as the first character:
echo file[^2].txt # file1.txt (file2 is excluded)6. Brace Expansion {a,b} and {1..10} — Not Globbing!
Brace expansion is not globbing. The shell generates strings from the braces before any filename matching, so echo file{1,2,3}.txt prints all three names whether or not the files exist. A real glob like file[1-3].txt only shows files that exist. They combine nicely: the shell expands the braces first, then matches the results as globs.
# BRACE EXPANSION is NOT globbing. The shell GENERATES strings from the
# braces BEFORE any filename matching — even if no file matches.
# {a,b,c}: one copy of the word per comma-separated item:
echo report.{txt,md,csv} # generated text, files need not exist
# Numeric sequence {1..5}: counts up:
echo file{1..5}.txt # prints all five names regardless of disk
# Compare: a GLOB only lists names that really exist on disk:
echo file{1..5}.txt # same braces...
echo file[1-5].txt # ...but the glob shows only real files
# Braces and globs combine — generate, THEN match:
ls {file,image}* # expands to file* image*, then lists matches7. Globstar, Tilde, Quoting, and nullglob
Four power features. ** matches across subdirectories, but only after shopt -s globstar. ~ expands to your home directory (the value of $HOME). Quoting or escaping a pattern ("*.txt" or \*.txt) disables globbing so the literal text is passed along. And by default a glob that matches nothing is left literal — shopt -s nullglob makes it expand to nothing instead.
# ** GLOBSTAR: recurse into subdirectories — but ONLY after enabling it.
shopt -s globstar
echo **/*.txt # now matches this folder AND sub/deep.txt
# ~ TILDE expansion becomes your HOME directory (the value of $HOME):
echo ~ # e.g. /home/ada
echo ~/globdemo # a path under home
# QUOTING / ESCAPING disables globbing — the literal pattern is passed:
echo "*.txt" # quotes -> the star is NOT expanded
echo \*.txt # backslash escape -> also literal
# NULLGLOB: by default a non-matching glob is left LITERAL...
echo *.zip # no .zip files -> prints the pattern itself
shopt -s nullglob
echo *.zip # ...now it expands to NOTHING (blank line)
shopt -u nullglob🎯 Your Turn: match exactly one character
Fill in the blank marked _ using the # 👉 hint so only the single-digit text files match (leaving out file10.txt), then run it and check the Output panel matches.
# 🎯 YOUR TURN — match the single-digit text files only.
# We have file1.txt, file2.txt, and file10.txt in the folder.
# Use the wildcard that matches EXACTLY ONE character so file10.txt
# (which has two digits) is left out.
echo file_.txt # 👉 replace the _ with the single-char wildcard ?
# ✅ Expected output:
# file1.txt file2.txt🎯 Your Turn: disable globbing with quotes
Make echo print the literal pattern *.txt instead of expanding it, by quoting the argument. Fill in the blank, then run it.
# 🎯 YOUR TURN — pass the LITERAL pattern, do not let the shell expand it.
# Right now this expands *.txt to the matching filenames:
echo *.txt
# Make echo print the literal text *.txt instead, by QUOTING it.
echo ___ # 👉 replace ___ with "*.txt"
# ✅ Expected output of the second command:
# *.txtPro Tips
- 💡 Preview before you act. Run echo PATTERN (or ls PATTERN) first to see what a glob will match before you put it after rm — globs expand the same way for every command.
- 💡 Globs are the shell's job, regex is the tool's job. ls *.log uses a glob, but grep '^ERROR' *.log uses a glob for the filenames and a regex for the line content. Don't mix up the symbols.
- 💡 Let find do its own matching. In find . -name "*.txt" quote the pattern so the shell doesn't expand it first — find needs to walk every directory itself.
- 💡 Include dotfiles deliberately with shopt -s dotglob, or by writing the leading dot yourself (.*). Turn it back off with shopt -u dotglob.
Common Errors (and the fix)
- My program received *.txt literally — there were no matching files, so bash passed the pattern through unchanged. Either create matching files or enable shopt -s nullglob so an empty match expands to nothing.
- * didn't include my .env file — a bare * skips dotfiles by design. Write .* for the hidden ones, or turn on shopt -s dotglob.
- ** only matched the current folder — globstar is off by default. Run shopt -s globstar first to make ** recurse.
- My regex .*\.txt matched nothing as a glob — you mixed up the two languages. In a glob it's *.txt; the regex form belongs inside tools like grep, not on the command line as a filename.
- rm * deleted more than I expected — the shell expanded * to every non-hidden name before rm ran. Always preview with echo * first, and quote a literal star if you truly mean the character.
📋 Quick Reference
| Pattern | Matches | Notes |
|---|---|---|
| * | Any run of characters | Skips dotfiles; stays in one dir |
| ? | Exactly one character | One ? per character |
| [abc] | One char: a, b, or c | Explicit set |
| [a-z] | One char in a range | Locale-dependent |
| [!0-9] | One char NOT in the set | ^ also negates |
| {a,b,c} | Generated strings a b c | Not a glob; no files needed |
| {1..10} | Numeric sequence 1..10 | Brace expansion |
| ** | Recurse into subdirs | Needs shopt -s globstar |
| ~ | Your home directory | Expands to $HOME |
| "*.txt" | The literal text *.txt | Quoting disables globbing |
Mini-Challenge: put the wildcards together
No blanks this time — just a brief and an outline. Combine *, ?, bracket sets, brace expansion, and ** in the globdemo folder, and check your output against the hints in the comments.
# 🎯 MINI-CHALLENGE: put the wildcards together
# In the globdemo folder, write commands that:
#
# 1. List every file that ENDS in .txt OR .md using ONE brace expansion
# (hint: *.{txt,md}).
# 2. List only the single-digit text files using ? so file10.txt is
# excluded.
# 3. List names that do NOT start with a digit using a negated bracket
# set (hint: [!0-9]*).
# 4. Enable shopt -s globstar and list every .txt at ANY depth,
# including sub/deep.txt.
# 5. Show the DIFFERENCE between a glob and brace expansion by echoing
# file{7..9}.txt (generated even though none exist) versus
# file[7-9].txt (a glob that matches nothing).
# your commands here🎉 Lesson Complete!
- ✅ * matches any run of characters (but skips dotfiles); ? matches exactly one character
- ✅ Bracket sets [abc] / ranges [a-z] match one character; [!...] or [^...] negates the set
- ✅ Brace expansion {a,b} and {1..10} generates strings — it's NOT globbing and needs no matching files
- ✅ ** recurses after shopt -s globstar; ~ expands to your home ($HOME)
- ✅ Globbing matches filenames in the shell while regex matches text in tools; quote a pattern ("*.txt") to disable expansion, and nullglob makes an unmatched glob vanish
Practice quiz
In bash, what does the * wildcard match by default?
- Any run of characters in a filename, but NOT files that start with a dot
- Exactly one character
- Only files that start with a dot
- Any text including the / between directories
Answer: Any run of characters in a filename, but NOT files that start with a dot. A bare * matches any sequence of characters (including none) in one filename, but dotfiles are skipped unless you write the leading dot yourself or set dotglob.
Which wildcard matches exactly ONE character?
- *
- +
- ?
- .
Answer: ?. ? matches any single character. file?.txt matches file1.txt but not file12.txt.
What does the pattern [a-c]*.txt match?
- Any .txt file
- Files whose name starts with a, b, or c and ends in .txt
- Files named literally a-c.txt
- Only file named abc.txt
Answer: Files whose name starts with a, b, or c and ends in .txt. [a-c] is a character range that matches one character a, b, or c, so the pattern matches names beginning with a, b, or c that end in .txt.
How do you write a bracket expression that matches any single character EXCEPT a digit?
- [no 0-9]
- [0-9]!
- {!0-9}
- [!0-9] (or [^0-9])
Answer: [!0-9] (or [^0-9]). A ! (or ^) as the first character inside the brackets negates the set, so [!0-9] matches one character that is not a digit.
Which statement about brace expansion like {a,b,c} is TRUE?
- It only works if files matching each item exist
- It matches exactly one character like ?
- It is identical to a regex alternation and reads files
- It is NOT globbing; it generates the strings even when no matching file exists
Answer: It is NOT globbing; it generates the strings even when no matching file exists. Brace expansion is pure text generation done before globbing. echo a{1,2,3} prints a1 a2 a3 whether or not those files exist.
What does the numeric brace expansion {1..5} produce?
- 1 2 3 4 5
- Only 1 and 5
- An error in bash
- The single string 1..5
Answer: 1 2 3 4 5. Numeric sequence brace expansion {1..5} expands to 1 2 3 4 5.
What must you do before the ** globstar will match across directory levels in bash?
- Quote the pattern
- Enable it with shopt -s globstar
- Run set -e first
- Nothing, ** always recurses by default
Answer: Enable it with shopt -s globstar. In bash ** behaves like * unless you enable the globstar option with shopt -s globstar, which lets ** match files and directories recursively.
What does the tilde ~ expand to on its own?
- The current directory
- The root directory /
- A single character
- Your home directory, the value of HOME
Answer: Your home directory, the value of HOME. Tilde expansion turns a leading ~ into the path stored in HOME, so ~/notes becomes /home/you/notes.
How can you stop the shell from expanding a glob so a program receives the literal pattern *.txt?
- Use a capital STAR
- It is impossible to disable globbing
- Quote or escape it, for example "*.txt" or \*.txt
- Put a space after the star
Answer: Quote or escape it, for example "*.txt" or \*.txt. Globbing happens on unquoted words. Quoting ("*.txt") or escaping (\*.txt) passes the literal pattern through unchanged.
By default in bash, what happens when a glob like *.md matches no files?
- The command is skipped silently
- The pattern is passed through literally as the text *.md
- The shell prints an error and stops
- It expands to an empty string
Answer: The pattern is passed through literally as the text *.md. By default bash passes the unmatched pattern through literally as text; enabling shopt -s nullglob makes a non-matching glob expand to nothing instead.
Continue this course
- Previous: Environment Variables
- Next: Command Substitution and Expansion — Capture output with $(...), plus arithmetic and parameter expansion
- Quick reference: Command Line cheat sheet
Frequently asked questions
What is the difference between globbing and a regular expression?
Globbing is filename matching done by the SHELL before a command runs: patterns like *.txt expand to the names of files that exist. A regular expression is text matching used INSIDE tools like grep or sed against the content of lines, not the filesystem. The symbols also differ: in a glob, * means any run of characters and ? means one character, while in a regex * means zero-or-more of the previous item and . means any single character. So *.txt is a glob, but .*\.txt is the rough regex equivalent.
Why does my * not match hidden files like .bashrc?
By design, a leading * does not match files whose name begins with a dot, so dotfiles stay hidden from accidental matches. To include them, write the dot yourself (for example .* or .config*), or enable the dotglob option with shopt -s dotglob so * also matches dotfiles.
Why did echo *.zip print *.zip instead of nothing when I have no zip files?
That is bash default behavior: when a glob matches no files, the unexpanded pattern is passed through literally, so the program receives the text *.zip. If you would rather a non-matching glob disappear, enable shopt -s nullglob, after which an unmatched pattern expands to nothing. The failglob option instead makes an unmatched pattern an error.
Is brace expansion the same as a wildcard?
No. Brace expansion such as {a,b,c} or {1..10} simply GENERATES text strings and happens before globbing, whether or not any matching file exists, so echo file{1,2,3}.txt always prints three names. Wildcards like * and ? only expand to files that actually exist on disk. They are often combined: the shell expands the braces first, then matches the results as globs.
How do I stop the shell from expanding a wildcard when I want to pass it to a command?
Quote it or escape it. Writing "*.txt" or '*.txt' or \*.txt passes the literal pattern to the program instead of letting the shell expand it. This matters for commands like find . -name "*.txt", where find does its own matching and you do not want the shell to expand the star first.