Linux Basic — Regex with =~

3 minute read

Regex with =~

Bash has a built-in regex operator, =~, inside [[ … ]]. It uses POSIX Extended Regular Expressions (ERE) — the same flavour as grep -E, sed -E, awk. No fork, capture groups available, and it integrates perfectly with if/while.


The Basic Syntax

if [[ "$string" =~ regex ]]; then
    echo "matched"
fi

Examples:

ver="v1.2.3"

if [[ "$ver" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "looks like semver"
fi

ip="192.168.1.10"
if [[ "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then
    echo "ip-shaped"
fi

=~ returns true (exit 0) if the regex matches anywhere in the string. Use ^…$ anchors when you want full-string match.


The Critical Quoting Rule

Don’t quote the regex. Bash treats a quoted regex as a literal string:

re='^[0-9]+$'
[[ "$x" =~ $re ]]         # ✅ works — regex is in a variable

[[ "$x" =~ "^[0-9]+$" ]]  # ❌ broken — quoted means literal
[[ "$x" =~ ^[0-9]+$ ]]    # ✅ works — direct, unquoted

If the regex has spaces or shell meta-characters, put it in a variable first and reference unquoted.

re='^Mozilla/5.0 .*Linux'
[[ "$ua" =~ $re ]]

The general guidance: store regex in a variable, reference unquoted; quote the string being matched.


Capture Groups — BASH_REMATCH

When the regex contains (…) groups, Bash populates the BASH_REMATCH array on match:

  • BASH_REMATCH[0] — the whole match
  • BASH_REMATCH[1] — first group
  • BASH_REMATCH[2] — second group
  • …and so on
ver="v2.7.18-rc4"
re='^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-z0-9]+)?$'

if [[ "$ver" =~ $re ]]; then
    major=${BASH_REMATCH[1]}
    minor=${BASH_REMATCH[2]}
    patch=${BASH_REMATCH[3]}
    pre=${BASH_REMATCH[4]}
    echo "major=$major minor=$minor patch=$patch pre=${pre:-none}"
fi
# major=2 minor=7 patch=18 pre=-rc4

No fork. No sed -E. Pure Bash.


ERE Flavour Reference

Pattern Meaning
. any single char
* + ? 0+, 1+, 0/1 of previous
{n} {n,} {n,m} repetition
^ $ start / end
[abc] [^abc] char class / negation
[[:alpha:]] POSIX class (alpha/digit/space/alnum/lower/upper/punct/xdigit)
(a\|b) alternation
(…) capture group
\. \\ escape literal . and \

What ERE does NOT support (vs PCRE/Perl):

  • \d \s \w — use [[:digit:]], [[:space:]], etc.
  • Lookaheads (?=…) / lookbehinds (?<=…)
  • Non-greedy *?, +?
  • Named groups (?<name>…)

If you need those, use grep -P, Python, or perl.


Useful Recipes

Validate a positive integer

[[ "$n" =~ ^[0-9]+$ ]] || { echo "Not a number"; exit 1; }

Validate an email (simple, not RFC-correct)

re='^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
[[ "$email" =~ $re ]] && echo valid

Strip and split a Bearer <token> header

header="Bearer eyJhbGciOiJIUzI1NiJ9.abc.def"
if [[ "$header" =~ ^Bearer[[:space:]]+(.+)$ ]]; then
    token=${BASH_REMATCH[1]}
fi

Parse key=value line

line='timeout = 30'
if [[ "$line" =~ ^[[:space:]]*([A-Za-z_]+)[[:space:]]*=[[:space:]]*(.+)[[:space:]]*$ ]]; then
    key=${BASH_REMATCH[1]}
    val=${BASH_REMATCH[2]}
fi

Find a version string in arbitrary text

msg="upgrading to version 4.12.0 now"
if [[ "$msg" =~ version[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+) ]]; then
    echo "version is ${BASH_REMATCH[1]}"
fi
# version is 4.12.0

When =~ Is Better Than grep

grep requires a pipe and a fork. =~ is in-process. So in tight loops:

# Slow — 10k forks
for line in "${lines[@]}"; do
    echo "$line" | grep -q '^ERROR' && (( err++ ))
done

# Fast — no forks
for line in "${lines[@]}"; do
    [[ "$line" =~ ^ERROR ]] && (( err++ ))
done

grep is still the right tool for streaming through large files. Use =~ for working with strings you already have in memory.


When NOT to Use =~

Need Better tool
Anchored multi-line input awk / sed
PCRE features (\d, lookarounds) grep -P or perl/python
Replacing matches sed, ${var//old/new}, or sed -E
Pattern across many lines awk (uses RS for record separator)

=~ matches; it doesn’t substitute. Pair it with parameter expansion (last chapter) for substitution.


Locale Note

[[:alpha:]] etc. depend on LC_CTYPE. Under a non-C locale, [A-Z] may include accented letters. In scripts that must be deterministic:

LC_ALL=C [[ "$s" =~ ^[A-Z]+$ ]]

Or set LC_ALL=C at the top of the whole script.


Common Mistakes

❌ Wrong ✅ Right Issue
[[ "$x" =~ "^[0-9]+$" ]] unquote: [[ "$x" =~ ^[0-9]+$ ]] quoted regex is literal
[[ "$x" =~ \d+ ]] (PCRE) [[ "$x" =~ [0-9]+ ]] ERE doesn’t know \d
Forgetting anchors ^…$ for full-string otherwise partial match wins
Using groups but not checking BASH_REMATCH always test the [[ … =~ … ]] first array is stale otherwise
Single-[ test ([ x =~ y ]) use [[ ]] =~ is a [[ ]] feature

Next → Practical Patterns

Updated: