| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user |
| Scope | [[ string =~ regex ]], POSIX extended regular expressions (ERE), capture groups with BASH_REMATCH, regex variables, RHS quoting, backslash escaping, case-insensitive matching, and exit status 2 on invalid patterns. Does not cover PCRE, grep -P, or full input-validation algorithms for IPv4 or semantic versions. |
| Related guides | Bash shell script tutorial Shell scripting course hub |
Bash can match POSIX extended regular expressions directly inside [[ … ]] with the =~ operator. Parenthesized groups are stored in BASH_REMATCH, which lets a script extract matched text without calling grep or sed.
Matching is substring-based by default; use ^ and $ when the whole value must match. Bash =~ follows POSIX ERE rules, not PCRE. For simple contains, prefix, or suffix checks without regex metacharacters, see compare strings in Bash — glob patterns on == are often clearer.
Bash Regex in One Example
Pull a date out of a log line and capture the YYYY-MM-DD portion:
line='started 2026-09-09 10:15'
if [[ $line =~ ([0-9]{4}-[0-9]{2}-[0-9]{2}) ]]; then
echo "date=${BASH_REMATCH[1]}"
fidate=2026-09-09BASH_REMATCH[0] holds the full matched substring; BASH_REMATCH[1] holds the first parenthesized group. Save captures to your own variables before another =~ test replaces or clears the array.
[[ string =~ regex ]] Syntax
=~ works only inside Bash [[ … ]]. It is not available in POSIX [ … ] — see Bash vs sh when a script must stay POSIX-portable.
[[ string =~ regex ]]The left operand is the string to search. The right operand is an extended regular expression. When the pattern matches anywhere in the string, the conditional succeeds with exit status 0; otherwise it fails with status 1.
Combine it with Bash if, elif, else like any other [[ ]] test:
email='user@example.com'
if [[ $email =~ @ ]]; then
echo 'contains @'
ficontains @For bracket-level differences between [ ], [[ ]], and (( )), see Bash [ ] vs [[ ]] vs (( )).
Bash Uses POSIX ERE, Not PCRE
Bash =~ follows POSIX extended regular expression rules. That is not the same as PCRE (Perl-compatible regex) used by tools such as grep -P on some systems.
| Feature | Bash =~ (ERE) |
PCRE (grep -P, many languages) |
|---|---|---|
| Digit class | [0-9] or [[:digit:]] |
Often \d |
| Word boundary | Not portable as \b |
\b |
| Lookahead | No | (?=…) |
| Non-capturing groups | (?:…) not in ERE |
Supported |
| Lazy/non-greedy quantifiers | No *?, +? |
Supported |
A pattern that works in Python or JavaScript may need rewriting for Bash. POSIX ERE uses leftmost-longest matching rules; Bash does not provide PCRE-style lazy quantifiers such as .*?. Prefer POSIX character classes such as [[:digit:]] and [[:alpha:]] when you want locale-aware classes instead of raw ASCII ranges.
Substring Match vs ^...$ Full Match
By default, =~ succeeds when the pattern matches any substring. It does not require the whole string to match.
s=abc123
if [[ $s =~ [0-9]+ ]]; then
echo 'substring match'
fisubstring matchA line of digits anchored to the full string fails when other characters are present:
s=abc123
if [[ $s =~ ^[0-9]+$ ]]; then
echo 'full digits'
else
echo 'not full digits'
finot full digitsUse ^ at the start and $ at the end when you need whole-string
validation — for example checking that a variable contains only digits.
For focused examples that compare digit-only checks with substring,
prefix, suffix, and alphanumeric tests, see how to
check what a Bash string contains.
Capture Groups with BASH_REMATCH
Parentheses in the regex create capture groups. After a successful =~ match, Bash fills the BASH_REMATCH array:
| Index | Content |
|---|---|
[0] |
Full matched substring |
[1] |
First capture group |
[2] |
Second capture group |
| … | Additional groups |
Parse a key=value token from a log line:
line='level=error user=admin'
if [[ $line =~ ^([^=]+)=([^[:space:]]+) ]]; then
echo "key=${BASH_REMATCH[1]} val=${BASH_REMATCH[2]}"
fikey=level val=errorBASH_REMATCH is a special global Bash array. Do not declare local BASH_REMATCH inside a function; Bash documents that doing so can produce unexpected results.
Copy captures you need before evaluating another =~ expression. A later regex test can replace or clear BASH_REMATCH:
[[ abc123 =~ ([a-z]+)([0-9]+) ]]
echo "letters=${BASH_REMATCH[1]}"
[[ nope =~ ([0-9]+) ]]
declare -p BASH_REMATCHletters=abc
declare -a BASH_REMATCH=()A later =~ evaluation changes BASH_REMATCH: a successful match replaces it with new captures, while a failed match can clear it. Save BASH_REMATCH entries to your own variables when you still need them after more regex tests.
A shape regex can split major.minor.patch into parts. It does not prove the numbers are semantically valid — 999.999.999 still matches the pattern:
ver=2.10.3
if [[ $ver =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "major=${BASH_REMATCH[1]} minor=${BASH_REMATCH[2]} patch=${BASH_REMATCH[3]}"
fimajor=2 minor=10 patch=3For strict semantic-version ordering or range checks, compare parsed integers or use a dedicated tool — a regex alone cannot validate version semantics.
How to Get All Regex Matches in Bash
=~ records one match per evaluation; it is not a global-match operator. To find repeated occurrences in one string, evaluate the regex repeatedly inside a Bash while loop while removing the text through each match:
text='id=12 id=34 id=56'
re='[0-9]+'
while [[ $text =~ $re ]]; do
match=${BASH_REMATCH[0]}
printf '%s\n' "$match"
text=${text#*"$match"}
done12
34
56For large files or line-oriented extraction, grep -oE or another text-processing tool is usually more convenient than looping in the shell.
Store Regex in a Variable
Putting the pattern in a Bash variables assignment keeps complex expressions readable and makes backslash escaping easier.
date_re='^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
d=2026-09-09
if [[ $d =~ $date_re ]]; then
echo 'date format ok'
fidate format okWhen the regex is unquoted on the right of =~, Bash treats it as ERE syntax. When it comes from a variable expansion, metacharacters in the variable value are active — which is what you want for a stored pattern.
The RHS Quoting Trap
Quoting any part of the right-hand regex makes that quoted portion match literally. An unquoted pattern treats ^ and $ as anchors; a quoted pattern compares against literal characters.
s=hello
if [[ $s =~ ^hello$ ]]; then
echo 'unquoted ok'
fiunquoted okThe same test with a quoted regex fails because Bash looks for the literal text ^hello$:
s=hello
if [[ $s =~ "^hello$" ]]; then
echo 'quoted ok'
else
echo 'quoted failed'
fiquoted failedQuote only the parts you intend to be literal — usually by building the pattern in a variable, not by wrapping the entire ERE in double quotes on the RHS.
Escaping Backslashes Correctly
Shell quoting and regex backslash interpretation happen in separate stages — count backslashes for each layer. Storing the pattern in a variable is the most reliable approach.
Match a filename ending in .txt:
suffix_re='\.txt$'
file=report.txt
if [[ $file =~ $suffix_re ]]; then
echo 'dot-txt match'
fidot-txt matchSingle quotes preserve the backslash, so suffix_re='\.txt$' stores one backslash before the dot. The regex engine interprets \. as a literal period.
Patterns copied from regex101 or PCRE tools often use \d for digits. That token is not POSIX ERE syntax:
re='\d+'
s=abc123
if [[ $s =~ $re ]]; then
echo 'pcre match'
else
echo 'no PCRE \d'
fino PCRE \dUse [0-9] or [[:digit:]] instead of \d.
Case-Insensitive Matching
By default =~ is case-sensitive. Enable nocasematch for the current shell when you need case-insensitive ERE matching:
shopt -s nocasematch
if [[ HELLO =~ ^hello$ ]]; then
echo 'nocase ok'
fi
shopt -u nocasematchnocase oknocasematch also affects ==, !=, and =~ inside [[ ]]. Turn it off when the block ends so later tests do not inherit case folding. For exact string equality without shell options, ${var,,} may be simpler than enabling case folding for the whole comparison.
Regex Exit Status and Invalid Patterns
A successful =~ match returns exit status 0. No match returns 1. A syntactically invalid ERE returns 2 — see Bash exit codes for how if interprets these values.
[[ x =~ [invalid ]]
echo "exit=$?"exit=2Status 2 means the pattern itself is invalid, not that the string failed to match. If a regex is constructed dynamically, distinguish status 2 from status 1: 1 means no match, while 2 means the regex itself is invalid.
Regex vs Glob vs grep
| Tool | Syntax | Best for |
|---|---|---|
[[ s == *pat* ]] |
Shell glob on == |
Simple contains, prefix, suffix |
[[ s =~ ere ]] |
POSIX ERE | Captures, alternation, counted repeats in shell logic |
grep -E |
ERE on file/stream | Filtering lines from files or pipelines |
Use Bash globbing on == when * and ? are enough. Use =~ when the value is already in a shell variable and the result controls shell logic. Use grep -E when you are filtering lines from files or pipelines.
Common Regex Mistakes
| Mistake | What goes wrong | Fix |
|---|---|---|
| Quoted entire RHS | ^ and $ become literal |
Leave ERE unquoted or use a variable |
Expecting PCRE \d, \b |
Not valid in Bash ERE | Use [0-9], [[:digit:]], or explicit boundaries |
No ^ / $ on validation |
Substring match passes partial input | Anchor the full string when validating |
Reusing BASH_REMATCH later |
A later =~ evaluation can replace or clear the array |
Copy to your own variables immediately |
| Shape regex for IPv4 or semver | Accepts out-of-range values | Treat regex as format check only; validate ranges separately |
| Invalid ERE syntax | Exit status 2, not a clean false |
Test patterns in development |
Summary
Bash regex matching lives in [[ string =~ regex ]], uses POSIX ERE rather than PCRE, and fills BASH_REMATCH when parenthesized groups match. Substring matching is the default — add ^ and $ when the entire string must conform to the pattern.
Common problems are quoting the regex operand, using PCRE-only syntax such as \d, and reading BASH_REMATCH after another regex test has changed it. Store complex patterns in variables, save captures you still need, and treat exit status 2 as a syntax error in the pattern itself.
For simple text contains checks without regex syntax, prefer glob patterns on ==. Reach for =~ when you need capture groups, counted repetitions, or alternation that globs cannot express.
References
- Bash conditional expressions —
=~operator andBASH_REMATCH - Pattern matching — globs versus regex in
[[ ]] - Regular expression notation — POSIX ERE rules
- grep utility — ERE matching on input lines
Frequently Asked Questions
Why does a quoted regex not match?
Quoting the right-hand side of =~ makes Bash treat the pattern as literal text. Anchors such as ^ and $, quantifiers, and groups are not active inside quotes. Store the ERE in a variable and use [[ string =~ $var ]] instead of quoting the pattern on the RHS.
Is Bash regex PCRE?
No. Bash =~ uses POSIX extended regular expressions. Features such as \d, (?:…), and lookahead are not available. Use [0-9] or [[:digit:]] for digits, and grep -P only when you explicitly need PCRE on a supported system.
How do I access capture groups?
After a successful =~ match, read BASH_REMATCH[1], BASH_REMATCH[2], and so on. Index 0 is the full match. Copy values to your own variables before another =~ test replaces or clears the array.
Does =~ match the full string?
Not by default. =~ succeeds when the pattern matches any substring. Use ^ at the beginning and $ at the end of the pattern when the entire string must match.

