$linuxjunkies
>

Bash Globbing and Extended Glob Patterns

Master Bash globbing beyond * and ?: learn globstar recursive matching, extglob operators, brace expansion, and the gotchas that cause real-world bugs.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • Bash 4.0 or later (check with bash --version; macOS ships an old Bash 3.2 by default)
  • Basic familiarity with the command line and shell variable syntax
  • A terminal with write access to a test directory for safe experimentation

Bash's filename expansion—globbing—is one of the shell's most powerful features, yet most users stop at * and ?. Mastering globstar, extglob, and brace expansion lets you select, move, and manipulate file sets that would otherwise require a separate find invocation or a brittle sed pipeline. This guide walks through each mechanism with practical examples and highlights the gotchas that bite even experienced sysadmins.

Basic Glob Patterns (Quick Reference)

Before reaching for the advanced features, make sure the fundamentals are solid. These patterns work in any POSIX shell without extra configuration:

  • * — matches any string of characters, excluding /
  • ? — matches exactly one character
  • [abc] — matches one character from the set
  • [a-z] — matches one character in the range
  • [!abc] — matches one character not in the set

One critical default: globs do not match dotfiles (files beginning with .) unless you either set dotglob or prefix the pattern explicitly with a dot.

Brace Expansion

Brace expansion is not technically globbing—it does not consult the filesystem—but it is processed first and integrates tightly with glob patterns. It generates arbitrary strings from a template.

Comma-separated lists

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

That single command creates four directories. Brace expansion happens before any filesystem lookup, so it works even when the paths do not yet exist.

Sequences

# Numeric sequence
touch report_{01..12}.txt

# Alphabetic sequence
echo {a..f}
# Output: a b c d e f

# Stepped sequence (Bash 4+)
echo {0..20..5}
# Output: 0 5 10 15 20

Nested and combined expansion

echo {jpg,png,gif}-{small,large}
# Output: jpg-small jpg-large png-small png-large gif-small gif-large

Brace expansion does not suppress itself when no match exists—unlike a glob, it always emits output. Keep this in mind when chaining with commands that expect real paths.

Enabling Shell Options

The advanced glob features below require specific shell options. Use shopt to toggle them. Changes apply only to the current session unless you add them to ~/.bashrc.

# Check current state
shopt globstar extglob dotglob nullglob

# Enable
shopt -s globstar extglob dotglob nullglob

# Disable
shopt -u globstar extglob dotglob nullglob

globstar: Recursive Directory Matching

With shopt -s globstar active, ** matches any number of directories—including zero—making recursive file selection possible without find.

Finding files by extension recursively

shopt -s globstar

# All Python files under the current tree
ls **/*.py

# Count them
echo **/*.py | wc -w

Iterating safely with a loop

shopt -s globstar nullglob

for f in /var/log/**/*.log; do
  echo "Processing: $f"
done

The nullglob option is paired here intentionally: without it, if no .log files exist the literal string /var/log/**/*.log is passed to the loop body, which is almost always wrong.

Copying a filtered recursive set

shopt -s globstar
cp --parents **/*.conf /backup/configs/

extglob: Extended Matching Operators

Enable with shopt -s extglob. Extended globs add five pattern operators that behave like simplified regular expressions:

OperatorMeaning
?(pattern)Zero or one occurrence
*(pattern)Zero or more occurrences
+(pattern)One or more occurrences
@(pattern)Exactly one occurrence
!(pattern)Anything except this pattern

Match multiple extensions

shopt -s extglob

# List all images
ls *.@(jpg|jpeg|png|gif|webp)

# Move them
mv *.@(jpg|jpeg|png|gif|webp) ~/Pictures/

Exclude a pattern with negation

shopt -s extglob

# Everything except .log files
ls !(*.log)

# Everything except backup directories
rm -rf !(backup|.git)

Caution: The negation operator !() is powerful but can produce surprising results if nullglob is not set or if you have subdirectories you have not accounted for. Always test with echo or ls before using rm.

Strip complex suffixes in parameter expansion

Extglob patterns also work inside ${} parameter expansion, which is useful for filename manipulation without spawning a subprocess:

shopt -s extglob

filename="archive.tar.gz"
# Strip everything from the first dot onward
base="${filename%%.*}"
echo "$base"   # archive

# Strip a specific multi-part extension
echo "${filename%.+(tar.)gz}"   # archive

Common Gotchas

Unquoted globs in assignments and conditions

A glob that matches nothing—when nullglob is off—expands to the literal pattern string. This is the single most common source of glob bugs:

# BAD: if no *.bak exists, $files holds the string "*.bak"
files=*.bak
echo $files

# GOOD: use nullglob and an array
shopt -s nullglob
files=(*.bak)
echo "${#files[@]} backup files found"

Word splitting erases intent

Never store a glob result in a plain variable and then rely on word splitting to iterate it. Use an array:

shopt -s globstar nullglob

# Wrong - breaks on filenames with spaces
for f in $files; do ...; done

# Right
files=(**/*.conf)
for f in "${files[@]}"; do
  echo "$f"
done

dotglob and hidden files

# Without dotglob, .bashrc is NOT matched by *
ls *

# With dotglob it is
shopt -s dotglob
ls *

Be careful with dotglob when using rm *—you will also remove dotfiles including .gitignore, .env, and similar files.

Globs are not regular expressions

A common mistake is writing *.{log,txt} expecting RE alternation. That is actually brace expansion combined with a glob, and it works—but *.(log|txt) without extglob enabled does not match the pipe as alternation. Enable extglob first, or use @(log|txt) explicitly.

extglob inside double brackets

shopt -s extglob

# Pattern matching in [[ ]] uses extglob automatically
if [[ $filename == *.@(jpg|png) ]]; then
  echo "It is an image"
fi

Verification

After enabling options in ~/.bashrc, reload the file and confirm the options are active:

source ~/.bashrc
shopt globstar extglob nullglob dotglob

The output will show on or off for each option. A quick sanity check for globstar:

shopt -s globstar nullglob
files=(**/*.sh)
echo "Found ${#files[@]} shell scripts"

Troubleshooting

  • Pattern expands to literal string: nullglob is off and nothing matched. Either enable it or check that the path exists.
  • extglob patterns cause a syntax error: The option is not enabled in the current shell or script. Add shopt -s extglob before the pattern.
  • globstar is slow on large trees: ** descends every directory including node_modules, .git, and virtual environments. For production scripts on large trees, prefer find with -prune.
  • Globs not expanding in scripts: Scripts inherit the invoking shell's shopt state only partially. Always set required options explicitly at the top of any script that uses them.
  • Unexpected dotfile deletion: Unset dotglob (shopt -u dotglob) before running broad removal commands.
tested on:Ubuntu 24.04Fedora 40Arch 2024.05Debian 12

Frequently asked questions

Do globstar and extglob work in scripts, or only interactively?
They work in scripts, but scripts do not inherit interactive shell options automatically. Add shopt -s globstar extglob near the top of any script that needs them.
What is the difference between * and ** with globstar enabled?
A single * never crosses a directory boundary—it matches filenames in the current directory only. With globstar enabled, ** matches across zero or more directory levels, making it recursive.
Can I use extglob patterns in [[ ]] conditional tests?
Yes. Inside [[ ]], the == operator performs pattern matching and respects extglob operators when the option is enabled, so [[ $f == *.@(jpg|png) ]] works as expected.
Why does !(*.log) also delete directories when I use rm -rf !(*.log)?
The negation pattern matches any name that is not a .log file, which includes directories. Add a check with -f or combine with find -maxdepth 1 -type f if you only want to target regular files.
Is brace expansion available in sh or dash, or only Bash?
Brace expansion is a Bash (and Zsh/ksh) extension; it is not part of the POSIX sh specification and is not available in dash. Scripts with a #!/bin/sh shebang should avoid it for portability.

Related guides