Glob Patterns
Filename expansion, wildcards, and pattern matching.
Glob vs Regex
| Aspect | Glob | Regex |
|---|---|---|
Purpose |
Match filenames/paths |
Match text content |
Where used |
Shell, |
|
|
Zero or more of ANY char |
Zero or more of PREVIOUS char |
|
Literal dot |
Any single character |
Escaping |
|
|
# Glob: * means "any characters" ls *.txt # All .txt files # Regex: * means "zero or more of previous" grep 'a*' file # Zero or more 'a' characters
Basic Wildcards
| Pattern | Meaning | Example |
|---|---|---|
|
Zero or more characters |
|
|
Exactly one character |
|
|
One of the listed characters |
|
|
One character in range |
|
|
NOT one of listed |
|
|
NOT in range |
|
# All .adoc files
ls *.adoc
# Single char + .txt
ls ?.txt
# Files starting with a, b, or c
ls [abc]*
# Files NOT starting with a digit
ls [!0-9]*
# Three-letter files
ls ???
# Files with digit in second position
ls ?[0-9]*
Brace Expansion (Not Glob, But Related)
Brace expansion {a,b,c} is different from glob - it expands BEFORE shell processes:
# Brace expansion (generates strings)
echo {a,b,c}.txt # Output: a.txt b.txt c.txt
echo file{1..5}.txt # Output: file1.txt file2.txt ... file5.txt
echo {a..z} # Output: a b c ... z
# Combining with glob
ls *.{adoc,md,txt} # All .adoc, .md, and .txt files
cp file.{txt,bak} # Copy file.txt to file.bak
# Nested braces
echo {a,b{1,2}}.txt # Output: a.txt b1.txt b2.txt
# Sequences with step
echo {0..10..2} # Output: 0 2 4 6 8 10
echo {a..z..2} # Output: a c e g i k m o q s u w y
# Brace: Expands even if files don't exist
echo {foo,bar}.txt # Output: foo.txt bar.txt (always)
# Glob: Only matches existing files
echo [fb]*.txt # Output: (only existing matches)
Extended Glob (extglob)
Enable in bash: shopt -s extglob
Zsh has most of these by default.
| Pattern | Meaning | Regex Equivalent |
|---|---|---|
|
Zero or one |
|
|
Zero or more |
|
|
One or more |
|
|
Exactly one |
|
|
NOT pattern |
(negative lookahead) |
# Enable extglob (bash)
shopt -s extglob
# One or more digits
ls +([0-9]).txt # Matches 1.txt, 123.txt (not .txt)
# Zero or more letters
ls *([a-z]).txt # Matches .txt, a.txt, abc.txt
# Either .jpg or .png
ls *.@(jpg|png) # Matches photo.jpg, image.png
# NOT .git directory
ls -d !(*.git) # Everything except .git
# Files NOT ending in .bak
ls !(*.bak)
# Exactly one letter followed by digits
ls @([a-z])+([0-9]).txt # Matches a1.txt, b123.txt
# Zsh uses different syntax
setopt EXTENDED_GLOB
# Zero or more
ls (ab)#.txt # Zsh: # means zero or more
# Negation
ls ^*.bak # Zsh: ^ means NOT
ls *~*.bak # Zsh: ~ means except
Recursive Globbing (globstar)
** matches directories recursively.
# Enable in bash
shopt -s globstar
# All .adoc files recursively
ls **/*.adoc
# All files in all subdirectories
ls **/*
# All Python files under src/
ls src/**/*.py
# Zsh has this enabled by default
# Just use **/ for recursive descent
# Find all config files
ls **/*.{conf,cfg,ini,yaml,yml}
# All shell scripts
ls **/*.{sh,bash,zsh}
# All in specific directory tree
ls ~/atelier/**/*.adoc
# Count files recursively
ls **/*.adoc | wc -l
Glob in find
find -name uses glob patterns:
# Basic glob
find . -name "*.adoc"
# Character class
find . -name "[A-Z]*.adoc" # Starts with uppercase
# Single character
find . -name "???.txt" # 3-char name + .txt
# NOT pattern (use !)
find . ! -name "*.bak"
# Multiple patterns (OR)
find . \( -name "*.adoc" -o -name "*.md" \)
# Case insensitive
find . -iname "*.ADOC" # Matches .adoc, .ADOC, .Adoc
# -path matches full path (including directories)
find . -path "**/test/*.py"
# Exclude directories
find . -path "./.git" -prune -o -name "*.adoc" -print
# Multiple exclusions
find . \( -path "./.git" -o -path "./node_modules" \) -prune \
-o -name "*.js" -print
# -name: glob pattern, matches filename only
find . -name "CR-*.adoc"
# -regex: regex pattern, matches FULL PATH
find . -regextype posix-extended -regex ".*/CR-[0-9]{4}-[0-9]{2}-[0-9]{2}.*\.adoc"
# Note: -regex must match entire path from .
Dotfiles and Hidden Files
By default, * does NOT match files starting with .
# These do NOT match .bashrc
ls *
ls *.rc
# Explicitly match dotfiles
ls .* # All dotfiles
ls .*.rc # Dotfiles ending in .rc
ls .[!.]* # Dotfiles (exclude . and ..)
# Bash: include dotfiles in glob
shopt -s dotglob
ls * # Now includes dotfiles
# Zsh: include dotfiles
setopt GLOB_DOTS
ls * # Now includes dotfiles
Handling No Matches
By default, unmatched glob returns literal pattern:
# If no .xyz files exist:
ls *.xyz # Output: ls: cannot access '*.xyz': No such file
# Bash: nullglob - expand to nothing if no match
shopt -s nullglob
ls *.xyz # No error, empty result
# Bash: failglob - error if no match
shopt -s failglob
ls *.xyz # bash: no match: *.xyz
# Zsh: NULL_GLOB
setopt NULL_GLOB
ls *.xyz # No error, empty result
# Safe iteration over files
shopt -s nullglob
for f in *.adoc; do
echo "Processing: $f"
done
# No iteration if no matches (good!)
# Without nullglob:
for f in *.adoc; do
[[ -e "$f" ]] || continue # Skip if literal
echo "Processing: $f"
done
Pattern Comparison Table
| Want | Glob | Regex | Example |
|---|---|---|---|
Any characters |
|
|
|
Single character |
|
|
|
Character set |
|
|
Same syntax |
Range |
|
|
Same syntax |
Negation |
|
|
Different syntax |
Literal dot |
|
|
Dot is special in regex |
One or more |
|
|
Different syntax |
Zero or more |
|
|
Different meaning! |
Alternation |
|
|
Similar with extglob |
Drill 1: Find Change Requests
# All CR files
find ~/atelier/_bibliotheca -name "CR-*.adoc"
# CR files from 2026
find ~/atelier/_bibliotheca -name "CR-2026-*.adoc"
# CR files from March 2026
find ~/atelier/_bibliotheca -name "CR-2026-03-*.adoc"
Drill 2: Find Worklogs
# All worklogs
find ~/atelier/_bibliotheca -name "WRKLOG-*.adoc"
# Today's worklog (substitute date)
find ~/atelier/_bibliotheca -name "WRKLOG-2026-03-12.adoc"
# All 2026 worklogs
ls ~/atelier/_bibliotheca/**/WRKLOG-2026-*.adoc
Drill 3: Config Files
# All config files
find /etc -name "*.conf" 2>/dev/null | head -10
# YAML configs
find . -name "*.y?ml" # .yml and .yaml
# All config-ish files
find . -name "*.@(conf|cfg|ini|yaml|yml)" 2>/dev/null
# Or without extglob:
find . \( -name "*.conf" -o -name "*.cfg" -o -name "*.ini" -o -name "*.yaml" -o -name "*.yml" \)
Drill 4: Antora Structure
# All antora.yml files
find ~/atelier/_bibliotheca -name "antora.yml"
# All nav.adoc files
find ~/atelier/_bibliotheca -name "nav.adoc"
# All module directories
ls -d ~/atelier/_bibliotheca/*/docs/modules/*/
Drill 5: Backup Files
# Find backup files
find . -name "*.bak" -o -name "*~" -o -name "*.orig"
# Find and delete (careful!)
find . -name "*.bak" -delete
# Find but exclude .git
find . -path "./.git" -prune -o -name "*.bak" -print
Drill 6: Scripts by Type
# All shell scripts
find ~/atelier -name "*.sh"
# Python scripts
find ~/atelier -name "*.py"
# All executable scripts
find ~/atelier -type f -executable -name "*.sh"
# Scripts with specific shebang
find ~/atelier -type f -exec grep -l '^#!/bin/bash' {} \;
Quick Reference
* any characters (not .) *.txt ? single character ?.txt [abc] one of a, b, c [abc].txt [a-z] range a to z [a-z].txt [!abc] not a, b, c [!0-9]*
{a,b} expands to: a b
{1..5} expands to: 1 2 3 4 5
{a..z} expands to: a b c ... z
{1..10..2} expands to: 1 3 5 7 9
?(pat) zero or one file?(s).txt *(pat) zero or more file*(s).txt +(pat) one or more file+(s).txt @(pat) exactly one @(foo|bar).txt !(pat) not pattern !(*.bak)
shopt -s extglob # Enable extended glob shopt -s globstar # Enable ** recursive shopt -s dotglob # Include dotfiles in * shopt -s nullglob # Empty result if no match
-name "*.txt" # Glob, filename only -iname "*.txt" # Case insensitive -path "**/test/*" # Glob, full path -regex ".*\.txt" # Regex, full path