Skip to content
Regex with =~ and BASH_REMATCH
step 1/7

Reading — step 1 of 7

Learn

~3 min readProcess Control and Debugging

Bash 3+ has built-in regex support via the =~ operator. Combined with the BASH_REMATCH array for capture groups, it replaces many uses of grep and sed. Bash Reference Manual covers it; ABS Guide goes deeper.

Basic =~ matching

str="hello world"
if [[ "$str" =~ ^hello ]]; then
    echo "starts with hello"
fi

if [[ "$str" =~ wor ]]; then
    echo "contains 'wor'"
fi

=~ returns true if the right-hand pattern matches anywhere in the left-hand string. The pattern is an extended regex (ERE) — like grep -E.

Capture groups — BASH_REMATCH

email="[email protected]"
if [[ "$email" =~ ^([^@]+)@([^@]+)$ ]]; then
    user="${BASH_REMATCH[1]}"
    domain="${BASH_REMATCH[2]}"
    echo "user=$user domain=$domain"
fi
  • BASH_REMATCH[0] — entire match
  • BASH_REMATCH[1], [2], ... — capture groups

Caveat: BASH_REMATCH is set by the LAST regex match. Save immediately if you'll do another match.

Common patterns

# IP address validation:
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]

# UUID:
[[ "$uuid" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]

# Numeric:
[[ "$x" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]

# Semver-ish:
[[ "$v" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]

# Extract date parts:
if [[ "$date" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
    year="${BASH_REMATCH[1]}"
    month="${BASH_REMATCH[2]}"
    day="${BASH_REMATCH[3]}"
fi

Quoting matters — DON'T quote the regex

# WRONG — quotes treat regex as literal string
[[ "hello" =~ "hel.*" ]]      # matches literal "hel.*", not pattern

# RIGHT — unquoted regex
[[ "hello" =~ hel.* ]]

# OR — store regex in a variable:
pat='^hel.*'
[[ "hello" =~ $pat ]]         # variable expansion is OK

Bash 3.2+ defaults to literal matching when the regex is quoted. Counterintuitive — always store regexes in unquoted variables for clarity.

Globbing vs regex

Different worlds:

  • Glob (*.txt, ?, [abc]) — for filename matching with case, [[ "$x" == pattern ]]
  • Regex (.*, [0-9]+) — for =~

Don't confuse them. [[ "$x" == "*.txt" ]] is a literal string match unless you don't quote the pattern.

case with patterns — alternative to regex

case "$file" in
    *.tar.gz|*.tgz) tar xzf "$file" ;;
    *.zip)          unzip "$file" ;;
    *.tar)           tar xf "$file" ;;
    *)               echo "unknown" ;;
esac

Glob patterns inside case. Often simpler than regex when matching filename suffixes.

When NOT to use bash regex

For complex parsing (JSON, XML, source code), use a real tool:

  • jq for JSON
  • xmllint, xmlstarlet for XML
  • awk, grep -P (PCRE) for harder text patterns

Bash regex shines for simple validation and extraction. Don't try to write a parser.

Common mistakes

  • Quoting the regex — Bash 3.2+ treats quoted regex as literal. Use unquoted or store in a variable.
  • Forgetting [[ ... ]]=~ only works inside [[ ]], not [ ] or (( )).
  • Locale-sensitive ranges[A-Z] may match accented characters in some locales. Use [[:upper:]] or set LC_ALL=C.
  • BASH_REMATCH overwritten — only the most-recent match's captures are available. Save immediately.
  • Treating =~ like a global match — it returns the FIRST match only. For all matches, loop or use external tools.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…