Linux Basic — Glob Patterns

4 minute read

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/*.log matches names in /tmp only, not subdirectories
  • * does not match leading . unless shopt -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

Updated: