Linux Basic — Glob Patterns
Glob Patterns
“Globs” are Bash’s pattern-matching language for filenames. They’re not regex, they’re not brace expansion, and they have a few hidden powers (extended globs, **) that most scripters never use.
The Basic Operators
| Pattern | Matches |
|---|---|
* |
any string (zero or more chars), no /, no leading . |
? |
exactly one char |
[abc] |
one char from the set |
[!abc] or [^abc] |
one char not in the set |
[a-z] |
one char in the range |
[[:alpha:]] |
POSIX class (alpha/digit/space/alnum/lower/upper/punct/xdigit) |
ls *.txt # all .txt files
ls report?.log # report1.log, reportA.log (exactly one char)
ls [abc]* # files starting with a, b, or c
ls [!._]* # files NOT starting with . or _
ls [[:digit:]]* # files starting with a digit
Notes:
*does not cross/—*in/tmp/*.logmatches names in/tmponly, not subdirectories*does not match leading.unlessshopt -s dotglob?is exactly one char (not zero-or-one as in regex)
Brace Expansion ≠ Globs
# Brace expansion: text generation (file may not exist)
echo file{1,2,3}.txt
# file1.txt file2.txt file3.txt
echo {a..c}{1..3}
# a1 a2 a3 b1 b2 b3 c1 c2 c3
# Globs: file matching (must exist on disk)
ls file*.txt
Brace expansion runs before glob expansion. It generates words from a literal pattern, not from filesystem contents. Mix them:
ls *.{txt,md,log} # expand braces first → *.txt *.md *.log → then glob
** — globstar
shopt -s globstar
# All .py under cwd, recursively
ls **/*.py
# Same with hidden dirs included
shopt -s dotglob
ls **/*.py
** matches across directory levels. Without globstar enabled, ** is just *. Default off → no surprise breakage for old scripts.
Performance: globstar is fine for hundreds-of-thousands of files. For million-scale trees, prefer find.
Extended Globs — extglob
Turn them on with shopt -s extglob. Then you get five PCRE-like operators:
| Pattern | Means |
|---|---|
?(p) |
zero or one of p |
*(p) |
zero or more of p |
+(p) |
one or more of p |
@(a\|b) |
exactly one of a or b |
!(p) |
anything except p |
shopt -s extglob
# Files NOT ending in .bak
ls !(*.bak)
# Files ending in .jpg or .png
ls @(*.jpg|*.png)
# A name that's all digits
ls +([0-9])
!(p) is especially useful as “all except”:
# Delete every backup except 'good.bak'
rm !(good).bak
Glob Anchoring — There Is None
Unlike regex, globs are fully-anchored to the whole filename:
ls *.log # filename must END with .log
ls log* # filename must START with log
ls *log* # filename must CONTAIN log
You don’t need ^ and $.
Bracket Expressions
POSIX character classes inside […]:
[[:alpha:]] [[:digit:]] [[:alnum:]]
[[:space:]] [[:upper:]] [[:lower:]]
[[:punct:]] [[:xdigit:]] [[:cntrl:]]
Useful examples:
ls [[:upper:]]* # start with uppercase
ls *[[:digit:]] # end with a digit
ls [[:alnum:]]* # start with letter or digit
Inside […], a literal - must be first or last; ] must be first; !/^ mean negation only if first.
Globs Inside [[ … == … ]]
Bash’s == operator inside [[ ]] does glob matching (not regex):
file="report.txt"
[[ "$file" == *.txt ]] && echo "is text"
[[ "$file" == report.* ]] && echo "is a report"
[[ "$file" != *.bak ]] && echo "not a backup"
This is the cleanest way to test “does this string match this pattern?” — no fork to grep. Use it freely.
Extended globs also work here when shopt -s extglob is on:
shopt -s extglob
file="image.png"
[[ "$file" == @(*.png|*.jpg) ]] && echo "is image"
case Uses Globs Too
case "$file" in
*.gz) gunzip "$file" ;;
*.tar) tar xf "$file" ;;
*.tar.gz) tar xzf "$file" ;;
*.@(jpg|png|gif)) echo "image — skipping" ;;
*) echo "unknown: $file" ;;
esac
case patterns are also globs. They’re more readable than chained if [[ … == … ]] ladders.
Globs vs Regex — A Cheat Sheet
| Concept | Glob | Regex |
|---|---|---|
| Any sequence | * |
.* |
| Single char | ? |
. |
| Char class | [abc] |
[abc] |
| Negated class | [!abc] |
[^abc] |
| Zero or more of x | *(x) (extglob) |
x* |
| One or more of x | +(x) (extglob) |
x+ |
| Optional x | ?(x) (extglob) |
x? |
| Alternation | @(a\|b) (extglob) |
(a\|b) |
| Anchors | implicit (whole-string match) | ^…$ |
Globs are for filenames and string-matches-pattern tests. Regex (=~ from Chapter 10) is for richer matching needs.
A Real Example — Categorise Files
shopt -s nullglob extglob
images=( *.@(jpg|jpeg|png|gif|webp) )
videos=( *.@(mp4|mov|avi|mkv) )
docs=( *.@(md|txt|pdf|doc|docx) )
echo "Images: ${#images[@]}"
echo "Videos: ${#videos[@]}"
echo "Docs: ${#docs[@]}"
A single glob does what would take a long find -name chain.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
[[ "$f" =~ *.log ]] |
[[ "$f" == *.log ]] |
=~ is regex, == is glob |
**/*.log without globstar |
shopt -s globstar first |
** is just * otherwise |
Quoting the glob ("*.log") |
leave unquoted | quoting disables expansion |
[a-Z] (mixed case range) |
[a-zA-Z] or [[:alpha:]] |
locale-dependent surprises |
Counting on * to match dotfiles |
shopt -s dotglob or explicit .* * |
dotfiles excluded by default |
Next → find & xargs