Bash Pattern Matching: =~ Operator and Regular Expressions

Much of the string work in shell scripts is matching: does this filename end in .log, does this version number begin with v2, does this input contain only digits? Bash supports shell glob patterns, including the extglob extension, and POSIX extended regular expressions. Knowing which to use saves you from calling grep every time you want to ask a yes-or-no question about a string.
This guide explains how to use glob patterns, extglob, and the =~ regex operator inside [[ ]], including how to capture groups with BASH_REMATCH. Run the examples in Bash; [[ ]] and =~ are not POSIX sh syntax.
Glob Patterns vs Regex
Globs and regexes look alike but are not the same:
- A glob is what the shell uses when it expands
*.txtordata?.csv. Wildcards are*(any string),?(any single character), and[abc](a bracket expression matching one listed character). - A regex uses anchors (
^,$), quantifiers (+,*,{m,n}), and grouping ((...)). Its syntax differs from globs:*repeats the preceding expression, while.*matches any string.
Bash supports globs in filename expansion and in pattern-matching constructs (case, [[ string == pattern ]]). The =~ operator inside [[ ]] uses POSIX extended regex. A glob must match the entire string, while an unanchored regex can match just part of it.
Glob Matching with [[ ]]
The == operator inside [[ ]] treats the right-hand side as a glob pattern. Leave the wildcard unquoted to check whether a filename ends in .log:
file="server.log"
if [[ "$file" == *.log ]]; then
echo "log file"
filog fileThe *.log pattern matches server.log because * covers the filename before the suffix. To check a substring instead, surround it with *:
file="server.log.2026-01-15"
if [[ "$file" == *log* ]]; then
echo "contains 'log'"
ficontains 'log'Here, *log* matches the whole string with any characters before and after log. Use != to test that a string does not match a pattern, such as [[ "$file" != *.log ]].
For literal equality, quote the right-hand side. This also prevents wildcard characters inside a variable from becoming a pattern:
file="server.log"
expected="server.log"
if [[ "$file" == "$expected" ]]; then
echo "exact match"
fiThe example prints exact match. Quoting only part of a pattern makes that part literal while leaving unquoted wildcards active. For example, [[ "$file" == *"$substring"* ]] searches for the literal value of $substring. For other comparison operators, see our guide on comparing strings in Bash
.
Character Classes
Globs and regexes both support bracket expressions. For example, [0-9] matches one digit:
version="v2.5"
if [[ "$version" == v[0-9]* ]]; then
echo "version starts with v then a digit"
fiversion starts with v then a digitThe pattern matches a leading v, one digit, and any remaining characters. The * does not mean “more digits”; it can also match periods, letters, and other characters.
The POSIX named classes work in patterns too: [[:alpha:]], [[:digit:]], [[:alnum:]], [[:space:]], [[:upper:]], [[:lower:]], and [[:punct:]]. They describe character categories in the current locale. Here we check for a leading letter and a digit later in the string:
input="abc123"
if [[ "$input" == [[:alpha:]]*[[:digit:]]* ]]; then
echo "starts with a letter and contains a later digit"
fiThe example prints the message, but it also accepts a-7? because each * matches arbitrary characters. To require only letters followed by digits, use the anchored regex ^[[:alpha:]]+[[:digit:]]+$ with =~, as explained below.
Extended Globs
Turn on the extglob shell option to use extended glob patterns in filename expansion and case statements:
shopt -s extglobEnable the option before Bash parses a function or compound command containing those patterns. Put the shopt command on its own line before the construct. Modern Bash also supports extended glob matching on the right of == and != inside [[ ]] without enabling the option.
Extended globs provide these constructs:
?(pattern-list)matches zero or one occurrence.*(pattern-list)matches zero or more.+(pattern-list)matches one or more.@(pattern-list)matches one of the given patterns.!(pattern-list)matches anything except the patterns.
pattern-list is one or more patterns separated by |. This example checks a fixed list of names, so you can run it without creating any files:
shopt -s extglob
for f in report.jpg report.png notes.txt; do
case "$f" in
report.@(jpg|png|webp))
printf 'image: %s\n' "$f"
;;
esac
doneimage: report.jpg
image: report.pngThe two image names match report.@(jpg|png|webp), while notes.txt does not.
You can also exclude names during filename expansion. The following loop previews regular files in the current directory, excluding names ending in .tmp or .bak:
shopt -s extglob nullglob
for f in !(*.tmp|*.bak); do
[[ -f "$f" ]] || continue
printf 'would process: %s\n' "$f"
doneThe -f check skips directories. nullglob makes the loop run zero times if no names match, rather than treating the pattern as a filename. These shell options remain enabled in the current shell. With dotglob off (the default), hidden names are not included, and the pattern does not search subdirectories.
The =~ Regex Operator
Inside [[ ]], =~ matches the left-hand string against a POSIX extended regular expression on the right:
input="192.168.1.10"
if [[ "$input" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "looks like an IPv4 address"
filooks like an IPv4 addressThe pattern matches four groups of digits separated by literal periods. This is only a shape check: it also accepts 999.999.999.999 and does not validate octet ranges or leading zeros.
The anchors (^ and $) require the regex to match the whole string; without them, the pattern can match anywhere in the input. Bracket expressions ([0-9]) and quantifiers (+, *, ?, {m,n}) work with POSIX extended regex syntax.
Leave regex operators unquoted when you want Bash to interpret them. A fully quoted regex ("[0-9]+") is matched as a literal string. You can also store the pattern in a variable and expand that variable unquoted:
input="192.168.1.10"
pattern='^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'
if [[ "$input" =~ $pattern ]]; then
echo "match"
fiThis prints match. The single quotes in the assignment preserve the regex text, including its backslashes. The unquoted $pattern on the right of =~ lets Bash interpret that text as a regex. Inside [[ ]], the expansion does not undergo word splitting or filename expansion.
The test returns status 0 for a match, 1 for no match, and 2 for an invalid regex. POSIX ERE does not provide PCRE lookarounds, named captures, or non-capturing groups. Use [[:digit:]] or [0-9] for digits instead of the PCRE shorthand \d.
Capture Groups with BASH_REMATCH
=~ populates the BASH_REMATCH array with the matched substrings. BASH_REMATCH[0] is the full match, and subsequent indices are the capture groups in order:
input="version=2.5.3-rc1"
if [[ "$input" =~ version=([0-9]+)\.([0-9]+)\.([0-9]+)(-([a-z0-9]+))? ]]; then
major="${BASH_REMATCH[1]}"
minor="${BASH_REMATCH[2]}"
patch="${BASH_REMATCH[3]}"
suffix="${BASH_REMATCH[5]}"
echo "major=$major minor=$minor patch=$patch suffix=$suffix"
fimajor=2 minor=5 patch=3 suffix=rc1Groups 1 through 3 capture the version components. Group 4 contains -rc1, including the hyphen, and the nested group 5 contains rc1. If the optional suffix is absent, those captures are empty. This pattern searches for a version within the input; add ^ and $ if the entire value must match.
Read or copy the captures immediately after a successful test. A later successful =~ test replaces them, and a test that returns “no match” clears the array. Keep the test and the code reading BASH_REMATCH in the same shell, since a subshell does not update its parent shell’s array. For indexed array syntax, see our Bash arrays
guide.
The case Statement
For several alternatives, case reads better than chained if [[ ]] blocks. Each clause contains glob patterns, and ;; ends the statement after the matching clause runs. Here we classify a filename extension:
ext="png"
case "$ext" in
jpg|jpeg|png|webp)
echo "image"
;;
mp4|mkv|webm)
echo "video"
;;
md|txt|rst)
echo "text"
;;
*)
echo "unknown"
;;
esacThis prints image because png matches the first clause. The *) branch is the catch-all. In a case pattern, ^ and $ are not regex anchors, and a bare + is not a regex quantifier. Extended glob forms such as +(pattern) have their own syntax and require extglob here. See our Bash case statement
guide for argument parsing and other branching examples.
Match File Extensions
A common task is splitting a filename into name and extension. Both globs and =~ work; pick what reads better.
Use parameter expansion to split off the last extension:
file="report.tar.gz"
name="${file%.*}" # report.tar
ext="${file##*.}" # gzThe % operator strips the shortest suffix matching .*, leaving report.tar. The ## operator strips the longest prefix matching *., leaving gz. If there is no period, both expansions return the original value, so check the name before assuming it has an extension.
With =~, you can recognize several archive types and capture the base name:
file="report.tar.gz"
if [[ "$file" =~ ^(.+)\.(tar\.gz|tar\.bz2|zip|7z)$ ]]; then
base="${BASH_REMATCH[1]}"
archive_type="${BASH_REMATCH[2]}"
echo "archive base=$base type=$archive_type"
fiarchive base=report type=tar.gzThe second capture contains the complete tar.gz extension because it is one of the regex alternatives. Parameter expansion can also remove a known compound suffix directly: ${file%.tar.gz} gives report. Use the regex when you want to recognize several suffixes and capture which one matched. Our Bash parameter expansion
guide covers the other prefix and suffix operators.
Anchoring and Whole-String Matches
Without ^ and $, =~ finds the pattern anywhere in the string:
if [[ "abc1234def" =~ [0-9]+ ]]; then
echo "contains digits"
ficontains digitsThe test succeeds because 1234 is a matching substring. To require the entire string to be digits, anchor both ends:
if [[ "12345" =~ ^[0-9]+$ ]]; then
echo "all digits"
fiThis prints all digits. Unlike =~, glob matching with == already checks the whole string; use * around a literal substring when you want to allow surrounding characters.
Bash regex matching can span newlines. In particular, . matches a newline, and ^ and $ anchor the complete string rather than each line. You can check this with a string containing an actual newline:
input=$'a\nb'
pattern='^a.b$'
if [[ "$input" =~ $pattern ]]; then
echo "matched across a newline"
fiThis prints matched across a newline. The $'a\nb' assignment creates the newline; writing \n in an ordinary single-quoted regex does not create one. If you need to match each line separately, read and test one line at a time.
Quick Reference
For a printable quick reference, see the Bash cheatsheet .
| Syntax | Purpose |
|---|---|
[[ "$value" == *.log ]] | Match the entire value against a glob |
[[ "$value" == "$expected" ]] | Compare literal strings |
[[ "$value" != *.log ]] | Check that a value does not match a glob |
[[ "$value" =~ $pattern ]] | Find a regex match anywhere in a value |
pattern='^[0-9]+$' | Regex requiring one or more digits and nothing else |
${BASH_REMATCH[0]} | Read the complete regex match after a successful test |
${BASH_REMATCH[1]} | Read the first parenthesized capture |
${file%.tar.gz} | Remove a known compound suffix |
Troubleshooting
Quoted regex never matches
Quoting the entire right-hand side makes it literal. Store the regex in a variable and expand it as $pattern, not "$pattern", on the right of =~. You can still quote the string on the left.
Extglob patterns are a syntax error
For filename expansion or case, enable shopt -s extglob before Bash parses the construct containing the pattern. Enabling it inside the same function or compound command can be too late. For a bash -c command containing these patterns, use bash -O extglob -c '...' to enable the option before parsing.
Regex test returns status 2
The regex is syntactically invalid, and Bash prints an error such as [[: invalid regular expression. Check for unmatched brackets or parentheses and unsupported PCRE syntax such as (?:...). Bash may leave earlier captures in place after an invalid test, so read BASH_REMATCH only when the current test succeeds.
Captures are empty or belong to another match
Check the test result before reading BASH_REMATCH, and copy the values before another =~ test runs. If the match happens in ( ... ) or a command substitution, read the captures there rather than expecting them to reach the parent shell.
Conclusion
When you write a match, decide whether the whole value must conform to the pattern or whether a substring is enough, then check the right-hand quoting. If the match controls a script’s next action, put it inside an if statement and read any captures in the successful branch.
Tags
Linuxize Weekly Newsletter
A quick weekly roundup of new tutorials, news, and tips.
About the authors

Dejan Panovski
Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 1000+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.
View author page