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

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 -a

2. 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 *.png

3. 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 png

4. 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].txt

5. 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 matches

7. 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:
#    *.txt

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

PatternMatchesNotes
*Any run of charactersSkips dotfiles; stays in one dir
?Exactly one characterOne ? per character
[abc]One char: a, b, or cExplicit set
[a-z]One char in a rangeLocale-dependent
[!0-9]One char NOT in the set^ also negates
{a,b,c}Generated strings a b cNot a glob; no files needed
{1..10}Numeric sequence 1..10Brace expansion
**Recurse into subdirsNeeds shopt -s globstar
~Your home directoryExpands to $HOME
"*.txt"The literal text *.txtQuoting 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!

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

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.