Regex vs Glob Patterns and When to Use Each With Examples
Compare regex and glob patterns: what * and ? mean in each, who expands them, and the common mix-ups with grep and find.

The same characters mean different things in the two languages, and the confusion causes real bugs: grep *.log file does not do what it appears to, and ls .* lists more than hidden files. Both are pattern languages, with different jobs. The examples use Bash 5.2 and GNU grep 3.11 and findutils 4.10; the engines are compared in PCRE versus RE2 versus POSIX.
Side by side
meaning glob regex (ERE)
any characters * .*
exactly one character ? .
one of a, b, c [abc] [abc]
not a, b, c [!abc] [^abc]
literal dot . \.
one or more (not avail.) +
alternatives {a,b} * a|b
* brace expansion done by Bash, not a glob feature
A glob matches whole names and is anchored by nature: *.txt must match the entire file name. A regex matches anywhere in the text unless anchored with ^ and $. A glob has no repetition operators beyond *, which makes it simple, and a regex can express almost any text shape.
Who expands what
ls *.txt # the shell expands the glob before ls runs
grep 'error.*timeout' *.log # quotes keep the regex away from the shell
grep error.*timeout *.log # the shell tries to expand error.*timeout as a globThe shell expands unquoted globs into a list of matching file names before the command starts, so the command never sees the pattern. If nothing matches, Bash passes the pattern unchanged, unless nullglob or failglob is set. A regex is passed to the tool as text and interpreted there, and it must be quoted, or characters such as *, ? and [ are given to the shell first. The classic mistake is grep *.log file, which expands *.log into file names and treats the first as the pattern.
find: two flavours
find . -name '*.log' # glob on the file name
find . -iname 'report-??.csv'
find . -regex '.*/report-[0-9]+\.csv' # regex on the whole path
find . -regextype posix-extended -regex '.*/(a|b)\.log'-name takes a glob matched against the base name, and quoting stops the shell from expanding it first. -regex matches the whole path, including the directory part, so a pattern without .*/ in front never matches. The default regex type is Emacs-style in GNU find, so use -regextype for extended syntax. More in find with regex and prune.
In between: Bash patterns
shopt -s extglob
[[ $f == *.@(jpg|png) ]] && echo image
case $f in +([0-9]).txt) echo numbered ;; esac
[[ $f =~ ^[0-9]+\.txt$ ]] && echo numberedWith extglob, Bash adds glob forms such as @(a|b), +(x) and !(x) that approach regex power, usable in case, [[ == ]] and filename expansion. =~ uses real regex. Choose the glob forms for file names and simple tests, and a regex when you need repetition or captures. The forms are in Bash extglob.
Why the same character means two things
Globs and regular expressions grew up for different jobs. A glob answers the question "which names in this directory match?", so it was designed for short, whole-name patterns with a few wildcards. A regular expression answers "does this stretch of text contain a shape like this?", so it needed repetition, alternation, grouping and anchors. The asterisk shows the gap clearly. In a glob it is a complete wildcard that stands for any run of characters. In a regex it is a quantifier that means "zero or more of the previous item", and it has nothing to repeat if it comes first. That is why grep *.log is odd: if the shell finds no matching file, the pattern reaches grep as *.log, a regex that starts with a quantifier, and most implementations treat the leading star as a literal.
The dot makes the other classic mistake. In a glob it is an ordinary character, so *.txt ends in a literal dot. In a regex it matches any one character, so report.txt as a regex also matches reportXtxt. Escaping it, report\.txt, is the habit that prevents false matches in log searches and in configuration for tools such as nginx and Apache, which use regexes for location rules.
Hidden files and the dot-star trap
The pattern .* in a glob matches every name that starts with a dot, which includes the two special entries . and .. in older shells. Running rm -r .* in such a shell walks up into the parent directory, with results that are easy to imagine. Modern Bash excludes those two entries from glob expansion since version 5.2 when the globskipdots option is on, and it is on by default there, but scripts that might run on older systems should use .[!.]* and ..?* or a find expression instead. By default * does not match names that start with a dot, and shopt -s dotglob changes that, which is why a loop over * misses a .env file.
Case, locale and character classes
Ranges such as [a-z] are defined by collation order in the current locale. In some locales the range includes uppercase letters, which gives surprising matches in both globs and regexes. POSIX character classes, written [[:lower:]], [[:digit:]] and [[:space:]], say what they mean and work in both languages. Case-insensitive matching is a separate switch: shopt -s nocaseglob for globs, grep -i for regexes, and the -iname test in find.
Choosing between them
A short rule works for most cases. Use a glob when the thing being matched is a file name and the shape is "a prefix, some wildcards and an extension". Use a regex when the target is text inside a file, when a pattern needs a repeat count, alternatives or captures, or when the same pattern must run in several tools. When a task sits between the two, such as selecting files whose names are numbers of any length, the extended glob +([0-9]).txt gives a one-line answer in Bash, and find -regex gives a portable one.
The cost of the wrong choice shows up in maintenance. A long chain of globs with case branches is hard to read when each branch differs by one character class, and a regex is easier to compact. Conversely, a regex for a simple extension check is longer than needed and slower to review. Write the clearer one, and add a comment with two example names that should match and two that should not.
Testing a pattern before using it
Use echo or printf '%s\n' on a glob to see what the shell would pass: printf '%s\n' *.txt prints one name per line and shows an unexpanded pattern if nothing matched. For a regex, feed sample lines through grep -E 'pattern' and compare the matched set with the expectation, including lines that should be rejected. Doing both checks before attaching a pattern to a destructive command, such as rm or sed -i, takes under a minute and prevents the cases that are hard to undo.
Where this does not apply
Some tools mix the two: .gitignore uses glob-like patterns with extra rules, rsync excludes use globs, and sed addresses use regexes while its w command takes a file name. Check which a tool expects before writing the pattern. The shell's rules are documented in Bash pattern matching, and the POSIX definitions in pattern matching notation. See also glob and regex.


