Bash scripting guide · part 28 of 32
Globbing
How *, ? and [...] match file names, what happens when nothing matches, and the shell options nullglob, failglob, dotglob, globstar and extglob.
Globbing (also called pathname expansion) is the shell turning a pattern such as *.log into
the list of existing file names that match it. It happens after variable expansion and word
splitting, on every unquoted word that contains *, ? or [. The command never sees the
pattern, only the resulting names, already sorted.
| Pattern | Matches |
|---|---|
* |
any string, including the empty one |
? |
exactly one character |
[abc], [a-h] |
one character from the set or range |
[!abc] or [^abc] |
one character not in the set |
[[:digit:]], [[:upper:]] |
one character from a named class |
Globs match file names, not text, and two rules set them apart from regular expressions:
* never matches a /, and a leading . must be matched explicitly.
The basic patterns
Star, question mark and brackets
echo logs/app.log*
echo logs/app.log.?
echo data/[a-h]*
echo photos/[[:upper:]]*
logs/app.log logs/app.log.1 logs/app.log.1 data/config.json data/fruits.txt data/hosts.txt photos/IMG_2041.png
app.log* matches both log files, because * may match nothing. ? needs exactly one
character, so only app.log.1 qualifies. The range picks the data/ files whose names
start with a to h, and the class [[:upper:]] finds the one photo whose name starts with
a capital letter.
Quoting turns globbing off
A quoted pattern is just a string. This is why variables holding patterns behave differently
depending on quotes: an unquoted $pat is globbed after expansion, a quoted "$pat" is not.
Quoted and unquoted patterns
pat='*.md'
echo $pat
echo "$pat"
echo '*.md'
README.md *.md *.md
Unquoted, $pat expanded to *.md and then globbed to README.md. Quoted, it stayed the
literal text. Passing a pattern to find -name therefore needs quotes, so that find
receives the pattern instead of the shell's matches.
The no-match pitfall
When a pattern matches nothing, bash by default leaves the word as it is: the literal
*.jpg is passed to the command. POSIX requires this, and it surprises everyone at least
once, usually in a loop.
A loop over a pattern that matches nothing
for f in *.jpg; do
echo "processing $f"
done
processing *.jpg
There are no .jpg files, yet the loop ran once, with f set to the literal string
*.jpg. Any command inside would fail on a file that does not exist. The classic guard is
[ -e "$f" ] || continue as the first line of the loop; the options below are the
bash-specific fixes.
nullglob makes a non-matching pattern disappear
shopt -s nullglob
for f in *.jpg; do echo "processing $f"; done
jpgs=(*.jpg)
echo "found ${#jpgs[@]} jpg files"
found 0 jpg files
With nullglob, *.jpg expands to nothing: the loop body never runs and the array is
empty, so counting matches becomes reliable. This is the right setting for loops and
arrays.
The flip side of nullglob
shopt -s nullglob
ls *.jpg | head -n 3
README.md access.log data
The pattern vanished, so ls ran with no arguments and listed the whole current
directory, the opposite of what was asked. The same goes for rm -f *.tmp (harmless) or
grep foo *.log (which then waits on standard input). Enable nullglob around the loop
that needs it, not globally in every script.
failglob turns no match into an error
shopt -s failglob
echo "before"
ls *.jpg; echo "same line, skipped"
echo "next line runs, status $?"
before /bin/bash: line 3: no match: *.jpg next line runs, status 1
With failglob, a pattern with no matches is an error: the command is not run and bash
prints no match: *.jpg. In a non-interactive shell it also abandons the rest of that
input line, so the second echo never ran; execution continued on the next line with status
1.
dotglob, globstar and nocaseglob
dotglob lets * match names starting with a dot (never . or ..). globstar gives
** its recursive meaning: on its own as a path component, it matches any number of
directories, including none. Without globstar, ** behaves exactly like *.
nocaseglob makes matching case-insensitive.
Include dotfiles and recurse
shopt -s dotglob
echo *
echo "--- without globstar:"
echo **/*.py
shopt -s globstar
echo "--- with globstar:"
echo **/*.py
.env README.md access.log data file with spaces.txt link-to-notes logs notes.txt photos scripts src todo.txt --- without globstar: src/app.py --- with globstar: src/app.py src/lib/helpers.py
With dotglob, .env now appears in *. Without globstar, **/*.py is the same as
*/*.py and finds only src/app.py, one level down. With it, the search descends into
src/lib too. ** also matches hidden directories only when dotglob is on.
extglob: alternation and negation
shopt -s extglob adds five pattern operators that take a |-separated list:
| Pattern | Matches |
|---|---|
?(a\|b) |
zero or one of the patterns |
*(a\|b) |
zero or more |
+(a\|b) |
one or more |
@(a\|b) |
exactly one |
!(a\|b) |
anything except the patterns |
The option changes how bash parses code, so it must be enabled on a line before the one that uses the patterns, and outside the function that uses them. Turning it on and using it on the same line is a syntax error.
Match everything except some extensions
shopt -s extglob
echo data/!(*.json|*.csv)
echo logs/@(app|error).log
echo logs/app.log?(.1)
echo photos/+([a-z]).png
data/fruits.txt data/hosts.txt data/inventory.tsv data/numbers.txt data/poem.txt data/settings.conf data/windows.txt logs/app.log logs/error.log logs/app.log logs/app.log.1 photos/beach.png photos/forest.png
!(*.json|*.csv) lists the data files that are neither JSON nor CSV, something plain globs
cannot express. @(app|error) picks one of two names, ?(.1) makes the suffix optional,
and +([a-z]) requires one or more lowercase letters, which rules out IMG_2041.png. The
same operators work inside [[ $x == pattern ]] (where bash enables them automatically)
and in ${var#pattern}.
Sort order and file names that look like options
Matches are sorted according to the locale's collation. In this sandbox (C.UTF-8) that is
byte order, so uppercase names come before lowercase ones; in en_US.UTF-8 they would be
interleaved. Do not rely on the order for anything important.
A glob can also expand to a name starting with -, which the command then reads as an
option. Prefix patterns with ./ or end the options with --.
A file named -l changes what ls does
Setup run before this example
mkdir uploads && touch uploads/-l uploads/a.txt uploads/b.txtcd uploads
ls *
echo "--- with ./"
ls ./*
-rw-r--r-- 1 dev dev 0 Oct 31 20:22 a.txt -rw-r--r-- 1 dev dev 0 Oct 31 20:22 b.txt --- with ./ ./-l ./a.txt ./b.txt
* expanded to -l a.txt b.txt, and ls took -l as its long-listing option, so the
file named -l was never listed. With ./* every match starts with ./, nothing looks
like an option, and all three files appear. ls -- * would work too.
Related
lsList the contents of directories, or the details of individual files.findWalk a directory tree and print, or act on, every file that matches a set of tests.rmDelete files, and with -r whole directory trees. There is no undo.- Brace expansionGenerate lists of words with {a,b,c} and sequences with {1..10} and {01..10..2}, why {1..$n} does not work, and how braces differ from globs.
- QuotingSingle quotes, double quotes, backslashes and $'...', what each protects, and the unquoted-variable bugs they prevent, each shown failing and then fixed.
- Word splittingWhen bash splits expansion results into separate words, how IFS controls it, and why
for line in $(cat file)does not loop over lines. - Loopsfor over lists, files and ranges, C-style for, while and until, break and continue, retry loops, and redirecting a whole loop's input or output.
- ArraysIndexed arrays in bash, creating, reading, appending, looping, slicing and removing elements, and using them to build command lines safely.