Globbing in Bash: Using extglob, globstar and nullglob in Practice
AI generated
$_
#!/
Bash · Globbing · Filesystem · Pattern
Globbing in Bash
Using extglob, globstar and nullglob in practice, instead of falling for empty matches

Bash's built-in globbing can do far more than the simple asterisk pattern in most beginner scripts: shopt -s extglob unlocks extended patterns like alternation and negation, globstar enables recursive ** matching across whole directory trees, and nullglob prevents the classic bug where a pattern with no match gets passed through literally as a filename.

16 min read shopt · extglob · globstar · nullglob Bash 4.x · 5.x · Linux

1. What globbing is and where the standard wildcards stop

Globbing refers to the shell's ability to expand patterns like *.log or file?.txt on its own, before a command is even invoked, unlike regular expressions, which are interpreted by individual programs such as grep. Bash's standard repertoire covers * for any sequence of characters, ? for exactly one character, and character classes like [abc] or [0-9], which already covers most everyday file selection needs.

Once requirements get more complex, for example several alternative extensions in a single pattern or a recursive descent through subdirectories, standard globbing hits its limits. That is exactly where the three shopt options extglob, globstar, and nullglob come in, together turning Bash's globbing into a noticeably more powerful tool without ever calling an external program like find.

2. extglob: extended patterns with ?(...), @(...) and !(...)

With shopt -s extglob, Bash enables a set of extended pattern operators that look syntactically similar to regular expressions but remain pure globbing under the hood. @(pattern1|pattern2) matches exactly one of the given alternatives, ?(pattern) matches the pattern zero or one times, *(pattern) zero or more times, +(pattern) one or more times, and !(pattern) negates the pattern, matching everything that does not fit it.

In practice, @(...) is by far the most used operator, for example to capture several file extensions in one go, while !(...) is handy for deliberately selecting everything except a specific file or pattern, something that would otherwise require combining find with grep -v without extglob.


#!/usr/bin/env bash
set -euo pipefail
shopt -s extglob

# Match multiple alternative extensions in one pattern
for f in *.@(jpg|jpeg|png|webp); do
  echo "Image: $f"
done

# Match everything EXCEPT a specific file
for f in !(README.md); do
  echo "Not the README: $f"
done

# Match one-or-more repetitions of a sub-pattern
for f in backup+([0-9]).tar.gz; do
  echo "Numbered backup: $f"
done

3. globstar: recursive ** matching across directory trees

By default, * in Bash never matches a directory separator, so a pattern like src/*.php does not automatically descend into subfolders. With shopt -s globstar, the behavior of two consecutive asterisks ** changes fundamentally: a pattern like src/**/*.php then matches every PHP file in src and in arbitrarily deeply nested subdirectories, functionally comparable to find src -name '*.php', but without spawning an external process.

An important difference from find is that globstar expands all matching paths for very large directory trees fully into memory before the command even starts, while find streams its results. With a few hundred thousand files, that can cost noticeably more memory and time, which is why globstar is best suited for project directories of manageable size, not for recursively scanning entire filesystems.


#!/usr/bin/env bash
set -euo pipefail
shopt -s globstar

# Match every .php file at any depth under src/
for f in src/**/*.php; do
  echo "PHP file: $f"
done

# Combine globstar with extglob for recursive multi-extension matching
shopt -s extglob
for f in src/**/*.@(php|phtml); do
  echo "PHP or phtml: $f"
done

4. nullglob: the pitfall of empty matches without this option

Without nullglob, Bash returns a pattern that matches no files completely unchanged, literally, instead of returning an empty list. In a loop like for f in *.bak; do ...; done, that means the loop body still runs exactly once, with the literal string *.bak as the value of $f, which is almost always a bug and leads to confusing follow-up errors in scripts without adequate checks, for example a delete command being applied to a nonexistent file literally named *.bak.

With shopt -s nullglob, a pattern with no match instead expands to an empty list, so the loop correctly does not run at all when nothing matches. This option belongs in practically every production Bash script that iterates over file lists using glob patterns, because it eliminates the single most common, and hardest to debug, globbing bug from the outset.


#!/usr/bin/env bash
set -euo pipefail

cd /tmp/empty-dir-without-bak-files 2>/dev/null || mkdir -p /tmp/empty-dir-without-bak-files && cd "$_"

echo "Without nullglob:"
for f in *.bak; do
  echo "  Loop ran with: $f"   # prints literally "*.bak" -- misleading!
done

shopt -s nullglob
echo "With nullglob:"
for f in *.bak; do
  echo "  Loop ran with: $f"   # never prints, loop body is skipped entirely
done
echo "Loop correctly skipped when nothing matched"

5. All three options combined: a robust deployment script

In practice, extglob, globstar, and nullglob are rarely used in isolation, they are usually enabled together at the top of a script because they complement each other: globstar provides recursive depth, extglob provides expressive power for pattern selection, and nullglob provides safety against empty matches. A typical example is a deployment script that recursively finds all configuration files with certain extensions while deliberately excluding test files.

It is worth bundling all shopt calls near the top of the script and documenting which options are active, because a reader familiar only with Bash's default settings will not recognize a pattern like !(*.test.php) as valid Bash syntax at all without knowing extglob is enabled.


#!/usr/bin/env bash
set -euo pipefail
shopt -s extglob globstar nullglob

readonly CONFIG_DIR="config"

# Recursively find all .yaml/.yml files, excluding anything with .test. in the name
for f in "$CONFIG_DIR"/**/!(*.test).@(yaml|yml); do
  echo "Deploying config: $f"
done

6. dotglob and failglob: two related, often overlooked options

Beyond the three core options, two related shopt switches are worth knowing: dotglob makes patterns like * also match hidden files whose name starts with a dot, which is excluded by default so that no * pattern accidentally includes . and ... Anyone who wants to include configuration files like .env or .gitignore in a glob pattern needs dotglob.

failglob is the exact opposite of nullglob: instead of returning an empty list when nothing matches, Bash aborts with an error message the moment a pattern fails to match anything. That fits scripts where a missing match should genuinely be a hard error condition, for example when an expected build artifact absolutely must exist before the next step is allowed to run.

7. Security: glob results and untrusted filenames

An often overlooked risk with globbing is that an expanded pattern can produce filenames starting with a dash, such as -rf. If such a name is passed unprotected to a command like rm $files, it can accidentally be interpreted as a command-line option instead of a filename. That is why every variable coming from a glob result should always be used in quotes, and for calls like rm, adding the option terminator (--) before the file list forces everything after it to be treated as a filename rather than an option, which adds extra safety.

A second risk involves filenames containing embedded spaces or newlines: if the result of a glob pattern is written into a loop variable without quotes and then processed through word splitting, Bash incorrectly tears a single filename with spaces apart into multiple words. The safe approach is iterating directly over the array that for f in *.log; do ... done already provides, and never storing the result as a plain, unquoted string in between.

8. Performance: globbing vs. find on large directory trees

For small to medium project directories with a few thousand files, globstar-based globbing is usually fast enough and has the advantage of needing no external process call at all, which is especially noticeable in loops that run many times. On directory trees with hundreds of thousands of files or deeply nested network filesystems, find shows clear advantages instead, because it streams results and can build in additional filters like modification date or file size directly, without loading the entire result set into memory first.

A proven rule of thumb is using globstar for project directories and deployment scripts whose size you know and control, and switching to find once a script needs to run on arbitrarily large, externally managed directory trees, for example a generic backup or cleanup routine running across different servers with an unknown file count.

9. Which glob option applies when

The right combination depends on the concrete use case: simple pattern lists with several extensions only need extglob, recursive searches across subdirectories additionally need globstar, and every loop iterating over a potentially empty result set with a glob pattern should generally enable nullglob to avoid the classic bug with the literal pattern string.

Option Enabled with Effect Typical use
extglob shopt -s extglob Extended patterns: @(), ?(), !(), +() Multiple extensions, negating patterns
globstar shopt -s globstar ** matches recursively across directories Project-wide file search without find
nullglob shopt -s nullglob Pattern with no match becomes an empty list Safe loops over glob results
dotglob shopt -s dotglob * also matches hidden files Capturing config files like .env
failglob shopt -s failglob No match aborts with an error Requiring expected files to exist

Mironsoft

Shell automation, DevOps tooling and deployment infrastructure

Shell scripts that hold up in production?

We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.

Code Review

ShellCheck analysis and manual review for critical Bash pattern violations.

Refactoring

Retrofitting error handling, logging and safe file operations.

CI Integration

Wiring ShellCheck and BATS into pipelines and building regression tests.

10. Summary

Globbing in Bash: The Essentials at a Glance

extglob

Enables @(), ?(), !(), *() and +() for extended pattern alternation and negation, syntactically close to extended regular expressions.

globstar

A double asterisk ** matches recursively across arbitrarily deep subdirectories, with no external find call required.

nullglob

Prevents the classic bug where a pattern with no match ends up as a literal filename inside the loop.

Combination

extglob, globstar and nullglob are usually enabled together at the top of a script and complement each other.

11. FAQ: Globbing in Bash: The Essentials at a Glance

1What exactly does shopt -s extglob do?
extglob enables extended pattern operators such as @(a|b), ?(pattern), *(pattern), +(pattern), and !(pattern), letting you express alternation, optional repetition, and negation within a single glob pattern.
2How does globstar differ from a plain *?
A single * never matches a directory separator, so it does not descend into subfolders. After shopt -s globstar, ** matches recursively across arbitrarily deeply nested directories.
3Why should I enable nullglob in every script?
Without nullglob, a pattern with no match returns itself as a literal string, which makes loops run once incorrectly. nullglob instead produces a correctly empty result list.
4Can I enable extglob, globstar and nullglob at the same time?
Yes, all three can be set in a single shopt call: shopt -s extglob globstar nullglob. They do not conflict and in practice complement each other very well.
5Is globstar faster than find?
For small to medium directory trees, globstar is often faster because no external process is spawned. For very large directory trees, find streams results and is more memory-efficient instead.
6What is the difference between nullglob and failglob?
nullglob returns an empty list when nothing matches and the script keeps running normally. failglob instead aborts with an error message the moment a pattern matches no file.
7Why doesn't my * pattern pick up hidden files?
Bash excludes files whose name starts with a dot from * by default, to prevent accidentally matching . and ... shopt -s dotglob turns that behavior off.
8Do extglob patterns also work in case statements?
Yes, once extglob is enabled, the same extended patterns like @(...) or !(...) can also be used as case patterns, making case branches noticeably more compact.
9Do shopt settings stay active for the whole script?
Yes, an option set with shopt -s applies for the entire running shell session or script, until it is explicitly turned off again with shopt -u.
10Do I need globstar for simple directory patterns like src/*.php?
No, standard globbing is entirely sufficient for patterns without recursive descent. globstar is only needed for the double asterisk ** that matches across subdirectories.