Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 2 – How Bash reads a command line

Globbing: wildcards, dotfiles, extglob, globstar

Match files with *, ? and brackets, decide what happens when nothing matches, and use dotglob, globstar, extglob and Bash 5.3's GLOBSORT with care.

  • Beginner
  • 5 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 5 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Match files with *, ?, [...] and [!...] and predict what happens when nothing matches
  • Set nullglob, failglob, dotglob and globstar to control what a pattern matches
  • Use extended patterns such as !(...) and +(...) for matching and trimming

Before you start

What you’ll learn · Before you start

What you will learn

  • Match files with *, ?, [...] and [!...] and predict what happens when nothing matches
  • Set nullglob, failglob, dotglob and globstar to control what a pattern matches
  • Use extended patterns such as !(...) and +(...) for matching and trimming

Before you start

On this page

In a folder with no Markdown files, count *.md reports one word: the text *.md itself. A script that then runs rm *.md or loops over the result works on a file that does not exist, unless you choose what an unmatched pattern should do.

You will match files with the four basic patterns, decide what a pattern that matches nothing becomes, include hidden files on purpose, search folders with **, use extended patterns, and sort matches with Bash 5.3’s GLOBSORT.

Four pattern characters match file names, and an unmatched pattern stays as it is

After word splitting, Bash looks at each unquoted word for the characters *, ? and [. Such a word is a glob pattern: A word containing *, ? or [ that Bash replaces with the sorted names of the files it matches; it stays as written when nothing matches, unless nullglob or failglob is set., and filename expansion replaces it with the matching names, sorted. * matches any string, ? any one character, [ab] one of the listed characters and [!ab] any character not listed.

The session builds a folder with a.txt, b.txt, c.log, a hidden .hidden.txt and d e.txt, then counts what each pattern becomes:

Five files, the basic patterns and the options for no match Python · glob_lab_session.py
from session import run

# A scratch folder with five files, one of them hidden and one with a space. count prints how many words a pattern
# became and each word in brackets, for the basic patterns and for the shell options that change what a pattern does
# when nothing, or a hidden file, matches.
run("""
touch a.txt b.txt c.log .hidden.txt 'd e.txt'
count() { printf '%d:' "$#"; (( $# )) && printf ' [%s]' "$@"; printf '\\n'; }
count *.txt
count ?.txt
count [ab].*
count [!ab]*
count *.md
shopt -s nullglob; count *.md; shopt -u nullglob
!shopt -s failglob; count *.md
shopt -u failglob
shopt -s dotglob; count *.txt; shopt -u dotglob
count .*
count *.TXT
shopt -s nocaseglob; count *.TXT; shopt -u nocaseglob
""")

Output

$ touch a.txt b.txt c.log .hidden.txt 'd e.txt'
$ count() { printf '%d:' "$#"; (( $# )) && printf ' [%s]' "$@"; printf '\n'; }
$ count *.txt
3: [a.txt] [b.txt] [d e.txt]
$ count ?.txt
2: [a.txt] [b.txt]
$ count [ab].*
2: [a.txt] [b.txt]
$ count [!ab]*
2: [c.log] [d e.txt]
$ count *.md
1: [*.md]
$ shopt -s nullglob; count *.md; shopt -u nullglob
0:
$ shopt -s failglob; count *.md
bash: no match: *.md
[exit status 1]
$ shopt -u failglob
$ shopt -s dotglob; count *.txt; shopt -u dotglob
4: [.hidden.txt] [a.txt] [b.txt] [d e.txt]
$ count .*
1: [.hidden.txt]
$ count *.TXT
1: [*.TXT]
$ shopt -s nocaseglob; count *.TXT; shopt -u nocaseglob
3: [a.txt] [b.txt] [d e.txt]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 glob_lab_session.py

*.txt matched 3 names, and d e.txt arrived as one word with its space: filename expansion happens after word splitting, so a matched name is never split. *.txt did not match .hidden.txt, because a leading dot is matched only by a pattern that starts with a dot. And *.md, matching nothing, stayed as the literal word *.md.

The order of the names comes from the locale’s collation. The session runs with LC_ALL=C.UTF-8, where names sort by their bytes, so capital letters come before small ones; in a language locale such as en_US.UTF-8 they may be interleaved. A script that depends on the order should set the locale it expects. To match a pattern character literally, quote it: '*'.txt or \*.txt matches only a file whose name really starts with an asterisk.

The same pattern language works outside filename expansion, in case and in [[ … ]]: case "$f" in *.txt) compares the word with the pattern without looking at any folder, and [[ $f == *.txt ]] does the same. There a pattern never turns into names; it only answers yes or no.

Globs are not regular expressions: in a glob * means “any string”, while in a regular expression it means “repeat the previous item”. *.txt as a regular expression would be an error or would mean something quite different.

Trusting a pattern to match

Wrong: for f in *.md; do convert "$f"; done in a folder that may be empty, which runs once with f set to *.md. Right: turn on nullglob first, so the loop runs zero times, or check [ -e "$f" ] inside the loop.

Shell options decide what happens with no match and with hidden files

Bash’s shopt builtin switches options on with -s and off with -u, and four of them change filename expansion. In the session, nullglob made the unmatched *.md vanish, 0 words, and failglob made it an error, no match: *.md, with status 1. dotglob let *.txt match the hidden file as well, and nocaseglob let *.TXT match the three lower-case names that the plain pattern missed. Without it, .* matched only .hidden.txt: since Bash 5.2 the globskipdots option, on by default, keeps . and .. out of every match.

The options arrived over many releases, which matters when a script may meet an older Bash, such as the 3.2.57 that macOS ships as /bin/bash:

Timeline · 6 events in order When Bash gained each globbing feature Walk through the releases in order to see which options an older Bash, such as macOS's 3.2, already has.
  1. 2.02

    Extended patterns and case-insensitive globbing new options

    The extglob option brings ksh-style patterns such as !(…) and +(…), and nocaseglob makes filename expansion ignore case.

  2. 3.0

    failglob new option

    A pattern that matches nothing becomes an error instead of staying as the literal word, so a mistyped glob stops the command.

  3. 4.0

    globstar new option

    matches all files and folders recursively when globstar is on, so /*.log finds logs at any depth. macOS's /bin/bash 3.2.57 lacks it.

  4. 4.3

    globasciiranges new option

    Ranges such as [a-d] can be made to follow plain ASCII order instead of the locale's; since 5.0 this is the default.

  5. 5.2

    globskipdots new default

    Patterns never return . and .. unless they are matched explicitly. It is on by default, which is why .* found only .hidden.txt.

  6. 5.3

    GLOBSORT new variable

    A variable that sorts the matches by name, size, blocks, mtime, atime, ctime, numeric value or not at all, ascending or descending.

The timeline has 6 events, from 2.02 to 5.3.

globstar makes ** match folders at any depth, and an older Bash treats ** like *. To see an option’s state before you rely on it, shopt nullglob prints nullglob off or nullglob on, and shopt -p prints every option as a command that sets it back. Options set with shopt stay set until the shell exits, so a script that needs one for a single command should switch it off again afterwards, as the session did after each test. Which no-match behaviour to choose depends on the script: nullglob suits loops that may have nothing to do, while failglob suits commands that should never run with a literal pattern, such as rm.

Key idea

An unmatched pattern stays as its own text unless nullglob removes it or failglob makes it an error, so choose one before a script loops over a glob.

Checkpoint

Check: nullglob, failglob, dotglob and globskipdots · The four pattern characters

  1. Question 1 of 2 A cleanup script runs rm -- *.tmp in a folder that may hold no .tmp files. Which option makes the command stop instead of running with the literal text?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: failglob

    Choose the no-match behaviour on purpose: failglob for commands that must not run with a literal pattern, nullglob for loops.

  2. Question 2 of 2 The folder holds a.txt, b.txt, .hidden.txt and d e.txt. How many words does count *.txt print, without dotglob?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 3

    The session printed 3: [a.txt] [b.txt] [d e.txt].

globstar and extglob search deeper and match anything but a pattern

Two options add new kinds of pattern. With globstar, **/*.log matches log files in the folder and every folder below it. With extglob, five patterns from the Korn shell become available: ?(…) zero or one, *(…) any number, +(…) one or more, @(…) exactly one, and !(…) anything except the listed patterns:

Searching with **, and matching with extglob Python · extglob_session.py
from session import run

# globstar lets ** cross folders; extglob adds patterns such as "anything but" and "one or more", which also work in
# ${var//pattern/replacement}. extglob must be on before Bash reads the line that uses it, so it gets a line of its own.
run("""
mkdir -p logs/api logs/web
touch logs/app.log logs/api/req.log logs/web/err.log draft-1.txt draft-2.txt final.txt notes.txt
count() { printf '%d:' "$#"; (( $# )) && printf ' [%s]' "$@"; printf '\\n'; }
count logs/*.log
shopt -s globstar
count **/*.log
!bash -c 'shopt -s extglob; echo !(draft-*).txt'
shopt -s extglob
count !(draft-*).txt
count +([a-z]).txt
touch photo.png scan.jpg icon.gif
count *.@(png|jpg)
f=report-v12-final-3.txt; printf '%s\\n' "${f//+([0-9])/N}"
""")

Output

$ mkdir -p logs/api logs/web
$ touch logs/app.log logs/api/req.log logs/web/err.log draft-1.txt draft-2.txt final.txt notes.txt
$ count() { printf '%d:' "$#"; (( $# )) && printf ' [%s]' "$@"; printf '\n'; }
$ count logs/*.log
1: [logs/app.log]
$ shopt -s globstar
$ count **/*.log
3: [logs/api/req.log] [logs/app.log] [logs/web/err.log]
$ bash -c 'shopt -s extglob; echo !(draft-*).txt'
bash: -c: line 1: syntax error near unexpected token `('
bash: -c: line 1: `shopt -s extglob; echo !(draft-*).txt'
[exit status 2]
$ shopt -s extglob
$ count !(draft-*).txt
2: [final.txt] [notes.txt]
$ count +([a-z]).txt
2: [final.txt] [notes.txt]
$ touch photo.png scan.jpg icon.gif
$ count *.@(png|jpg)
2: [photo.png] [scan.jpg]
$ f=report-v12-final-3.txt; printf '%s\n' "${f//+([0-9])/N}"
report-vN-final-N.txt

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 extglob_session.py

logs/*.log found 1 file and **/*.log all 3. The manual describes ** as matching all files and zero or more folders, so a .log file at the top of the folder would have matched as well. !(draft-*).txt kept the two names that are not drafts, and +([a-z]).txt the names made only of letters. The last line used +([0-9]) inside ${f//pattern/N}, replacing each run of digits, because the same patterns work in parameter expansion and in [[ … ]].

The failed line in the session matters: extglob changes how Bash reads a line, so it must be on before the line that uses the pattern is read. Switched on in the same line, as in shopt -s extglob; echo !(draft-*).txt, the pattern is a syntax error. Put shopt -s extglob on a line of its own near the top of the script.

@(…) is the everyday one: *.@(png|jpg) matched the photo and the scan and left out the GIF, one pattern instead of two. Extended patterns can do much of what people reach for regular expressions to do, but they still match whole names, so +([a-z]).txt needed every character before .txt to be a letter.

GLOBSORT orders matches by size or time instead of name

Matches are normally sorted by name. Bash 5.3 adds the variable GLOBSORT, whose NEWS entry lists name, size, blocks, mtime, atime, ctime, numeric and none as the keys, in ascending or descending order. A leading - reverses the order:

Three files sorted by name, by size and by size reversed Python · globsort_session.py
from session import run

# Bash 5.3's GLOBSORT sorts the matches of a pattern by another key than the name; a leading - reverses the order.
# The three files are 5, 10 and 1 bytes long, so the name order and the two size orders all differ.
run("""
printf 'xxxxx' > apple.dat; printf 'xxxxxxxxxx' > kiwi.dat; printf 'x' > plum.dat
printf '[%s]\\n' *.dat
GLOBSORT=size; printf '[%s]\\n' *.dat
GLOBSORT=-size; printf '[%s]\\n' *.dat
unset GLOBSORT
""")

Output

$ printf 'xxxxx' > apple.dat; printf 'xxxxxxxxxx' > kiwi.dat; printf 'x' > plum.dat
$ printf '[%s]\n' *.dat
[apple.dat]
[kiwi.dat]
[plum.dat]
$ GLOBSORT=size; printf '[%s]\n' *.dat
[plum.dat]
[apple.dat]
[kiwi.dat]
$ GLOBSORT=-size; printf '[%s]\n' *.dat
[kiwi.dat]
[apple.dat]
[plum.dat]
$ unset GLOBSORT

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 globsort_session.py

By name the order was apple, kiwi, plum; with GLOBSORT=size it became plum, apple, kiwi, 1, 5 and 10 bytes, and with -size the reverse. The manual lists the other keys: mtime, atime and ctime for the three file times, so -mtime puts the newest file first, blocks for the space on disk, and numeric, which sorts names made only of digits by their value, so 10 comes after 9. When two files tie on the key, the name decides. Older versions of Bash ignore the variable, so a script that relies on it should check BASH_VERSINFO first.

Checkpoint

Check: globstar and extglob · GLOBSORT

  1. Question 1 of 2 Why did shopt -s extglob; echo !(draft-*).txt fail with a syntax error?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The whole line was read before shopt ran, and without extglob the pattern is not valid syntax

    Put shopt -s extglob on a line of its own near the top of the script.

  2. Question 2 of 2 apple.dat, kiwi.dat and plum.dat are 5, 10 and 1 bytes long. In which order does GLOBSORT=-size list *.dat?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: kiwi.dat, apple.dat, plum.dat

    The session printed [kiwi.dat] [apple.dat] [plum.dat] for -size and the reverse for size.

Practise: patterns against folders

The questions describe folders and ask which names a pattern matches, what an unmatched pattern does, and which option a script needs. The figure and the sessions hold every fact you need:

Practice that adapts to you

Practise this lesson

9 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 What does ? match in a glob pattern?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: Exactly one character

    *, ? and […] are the pattern characters; quote them to mean themselves.

  2. Question 2 of 9 In the session's folder, what did [!ab]* match?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: c.log and d e.txt

    The session printed 2: [c.log] [d e.txt].

  3. Question 3 of 9 A colleague writes grep '*.txt' files.list to find names ending in .txt. What is wrong?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: grep reads a regular expression, where * repeats the previous item, so this is not the glob they meant

    In a glob, * means any string; in a regular expression it means repeat. Keep the two apart.

  4. Question 4 of 9 With nullglob on, what does an unmatched pattern become?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: Nothing at all, zero words

    nullglob suits loops: a loop over an unmatched pattern then runs zero times.

  5. Question 5 of 9 In Bash 5.3.20, why did .* match only .hidden.txt and not . and ..?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: globskipdots, on by default since 5.2, keeps . and .. out of matches

    The NEWS file for 5.2 records globskipdots as a new option enabled by default.

  6. Question 6 of 9 With logs/app.log, logs/api/req.log and logs/web/err.log, how many names does **/*.log match with globstar on?

    Type a number.

    A hint

    ** crosses folders at any depth.

    Show the answer: Question 6 of 9

    Answer: 3 names

    logs/.log found 1 and */*.log all 3, as the session printed.

  7. Question 7 of 9 With extglob on, what does !(draft-*).txt match among draft-1.txt, draft-2.txt, final.txt and notes.txt?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: final.txt and notes.txt

    The session printed 2: [final.txt] [notes.txt].

  8. Question 8 of 9 f=report-v12-final-3.txt. With extglob on, what does "${f//+([0-9])/N}" give?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: report-vN-final-N.txt

    Extended patterns work in parameter expansion too, as the session's last line showed.

  9. Question 9 of 9 What does a Bash older than 5.3 do with GLOBSORT=size?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: Nothing special; it is an ordinary variable there, and matches stay sorted by name

    Check BASH_VERSINFO before relying on a 5.3 feature in a script that may meet an older Bash.

Interview questions

Warm-up (fresher to 2–5 years): what happens in Bash when a glob matches no files?

By default the pattern stays as the literal word, so ls *.md in a folder without Markdown files passes the text *.md to ls, which then reports that no such file exists. Two shell options change this. With nullglob the unmatched pattern expands to nothing at all, which suits loops that may have no work. With failglob it is an expansion error and the command does not run, which suits destructive commands such as rm. Whatever the setting, quote a pattern you mean literally, and check [ -e "$f" ] in a loop when you cannot change the options.

Two harder questions follow, about cleaning old logs safely and about a script that must also run on an older Bash. Make your plan, then open each first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A nightly job must delete .log files older than seven days in /var/app/logs and its subfolders. A teammate proposes cd /var/app/logs; rm **/*.log. What risks do you see, and what would you write instead?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    Your build script uses shopt -s globstar extglob and GLOBSORT=mtime. It must also run on Macs whose /bin/bash is 3.2.57. What breaks there, and how do you make the script reliable on both?

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Which characters make a word a glob pattern, and what does each match?

    * matches any string, ? one character, [...] one character from a set, and [!...] one character not in the set. A leading dot in a name must be matched explicitly.

  2. What does Bash do by default with a glob that matches no file?

    It leaves the pattern as its own text, so ls *.md in a folder without Markdown files gets the word *.md and reports that no such file exists.

  3. How do nullglob and failglob change an unmatched pattern?

    With nullglob it expands to nothing, which suits loops that may have no work. With failglob it is an expansion error and the command does not run, which suits destructive commands.

  4. What do the globstar and extglob options add?

    globstar makes ** match folders at any depth. extglob adds patterns such as !(...) for anything but a pattern and +(...) for one or more; it must be on before Bash reads the line that uses them.

  5. What does GLOBSORT change in Bash 5.3?

    The order of glob matches: they can be sorted by size, by time or by other keys instead of by name. Older versions ignore the variable.

Key takeaways

  • *, ?, […] and [!…] match file names after word splitting, so a matched name with spaces stays one word, and a leading dot must be matched explicitly.
  • A pattern that matches nothing stays as its own text unless nullglob removes it or failglob makes it an error, and globskipdots keeps . and .. out of matches since Bash 5.2.
  • globstar makes ** search folders at any depth and extglob adds patterns such as !(…) and +(…), which must be switched on before the line that uses them is read.
  • Bash 5.3’s GLOBSORT sorts matches by size, time or other keys, and older versions ignore it.

Terms in this lesson

glob pattern
A word containing *, ? or [ that Bash replaces with the sorted names of the files it matches; it stays as written when nothing matches, unless nullglob or failglob is set.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.