Linux Basic — Bash =~ Operator

2 minute read

Bash =~ Operator

Bash has a built-in regex engine inside the [[ ... ]] conditional. Use =~ to test a string against an ERE pattern with zero external processes and direct access to capture groups.

str="user-1234"
if [[ "$str" =~ ^user-([0-9]+)$ ]]; then
    echo "matched, id = ${BASH_REMATCH[1]}"
fi

Syntax Rules

[[ "$string" =~ regex_pattern ]]
  • Always inside [[ ]] (not [ ])
  • The left side is the string — quote it
  • The right side is the regex — don’t quote it (since Bash 3.2 with compat31 off, quoting forces literal matching)
# WRONG — quotes around regex make it literal
[[ "$x" =~ "^[0-9]+$" ]]      # only matches the literal "^[0-9]+$"

# RIGHT
[[ "$x" =~ ^[0-9]+$ ]]

If your regex has spaces or shell metacharacters, assign it to a variable first:

pat='^(error|warn): (.+)$'
[[ "$line" =~ $pat ]]         # unquoted on the right

Capture Groups: BASH_REMATCH

After a successful match, BASH_REMATCH holds the whole match in [0] and each (group) in [1], [2], …

ip="10.20.30.40"
if [[ "$ip" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
    echo "octets: ${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]} ${BASH_REMATCH[4]}"
fi

BASH_REMATCH is overwritten on every =~ — copy what you need immediately.


Common Patterns

Validate input

read -p "Port: " port
if ! [[ "$port" =~ ^[0-9]+$ ]]; then
    echo "not a number" >&2; exit 1
fi
(( port >= 1 && port <= 65535 )) || { echo "out of range" >&2; exit 1; }

Branch on type

case_str="$1"
if   [[ "$case_str" =~ ^[0-9]+$ ]];                  then echo "integer"
elif [[ "$case_str" =~ ^[0-9]+\.[0-9]+$ ]];          then echo "decimal"
elif [[ "$case_str" =~ ^[A-Za-z][A-Za-z0-9_]*$ ]];   then echo "identifier"
else                                                       echo "other"
fi

Pull a version from a tag

tag="release-v2.4.1-rc3"
if [[ "$tag" =~ v([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
    major="${BASH_REMATCH[1]}"
    minor="${BASH_REMATCH[2]}"
    patch="${BASH_REMATCH[3]}"
    echo "version $major.$minor.$patch"
fi

Why Use =~ Instead of grep?

=~ grep -Eq
no subprocess forks grep
gives you capture groups only yes/no (or use grep -oE)
Bash-only portable to any POSIX shell
ERE only ERE (with -E) or BRE

For scripts that are already Bash and need substring extraction, =~ is faster and cleaner. For portability or when you genuinely need to scan a file, use grep.


Pitfalls

Pitfall Fix
Quoting the regex Don’t — or assign to a var first
Special chars (., +, [) match literally Escape: \. \+ \[
BASH_REMATCH clobbered by next =~ Copy values to local vars right away
Not anchoring → false positives Use ^ and $
Inside ( ) subshell → captures lost in parent Stay in the same shell

Next → POSIX Character Classes

Updated: