Linux Basic — Regex with =~
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 matchBASH_REMATCH[1]— first groupBASH_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