Bash Pattern Matching: =~ Operator and Regular Expressions

By 

•

Published on

•

10 min read

Bash terminal with the =~ operator, a *.log pattern, and a matching server.log filename

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 *.txt or data?.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:

sh
file="server.log"

if [[ "$file" == *.log ]]; then
    echo "log file"
fi
output
log file

The *.log pattern matches server.log because * covers the filename before the suffix. To check a substring instead, surround it with *:

sh
file="server.log.2026-01-15"

if [[ "$file" == *log* ]]; then
    echo "contains 'log'"
fi
output
contains '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:

sh
file="server.log"
expected="server.log"

if [[ "$file" == "$expected" ]]; then
    echo "exact match"
fi

The 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:

sh
version="v2.5"

if [[ "$version" == v[0-9]* ]]; then
    echo "version starts with v then a digit"
fi
output
version starts with v then a digit

The 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:

sh
input="abc123"

if [[ "$input" == [[:alpha:]]*[[:digit:]]* ]]; then
    echo "starts with a letter and contains a later digit"
fi

The 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:

Terminal
shopt -s extglob

Enable 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:

sh
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
done
output
image: report.jpg
image: report.png

The 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:

sh
shopt -s extglob nullglob

for f in !(*.tmp|*.bak); do
    [[ -f "$f" ]] || continue
    printf 'would process: %s\n' "$f"
done

The -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:

sh
input="192.168.1.10"

if [[ "$input" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "looks like an IPv4 address"
fi
output
looks like an IPv4 address

The 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:

sh
input="192.168.1.10"
pattern='^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'

if [[ "$input" =~ $pattern ]]; then
    echo "match"
fi

This 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:

sh
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"
fi
output
major=2 minor=5 patch=3 suffix=rc1

Groups 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:

sh
ext="png"
case "$ext" in
    jpg|jpeg|png|webp)
        echo "image"
        ;;
    mp4|mkv|webm)
        echo "video"
        ;;
    md|txt|rst)
        echo "text"
        ;;
    *)
        echo "unknown"
        ;;
esac

This 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:

sh
file="report.tar.gz"
name="${file%.*}"     # report.tar
ext="${file##*.}"     # gz

The % 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:

sh
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"
fi
output
archive base=report type=tar.gz

The 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:

sh
if [[ "abc1234def" =~ [0-9]+ ]]; then
    echo "contains digits"
fi
output
contains digits

The test succeeds because 1234 is a matching substring. To require the entire string to be digits, anchor both ends:

sh
if [[ "12345" =~ ^[0-9]+$ ]]; then
    echo "all digits"
fi

This 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:

sh
input=$'a\nb'
pattern='^a.b$'

if [[ "$input" =~ $pattern ]]; then
    echo "matched across a newline"
fi

This 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 .

SyntaxPurpose
[[ "$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.

Linuxize Weekly Newsletter

A quick weekly roundup of new tutorials, news, and tips.

About the authors

Dejan Panovski

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