Regex Flavors
Why the same pattern can behave differently: POSIX basic and extended syntax, PCRE, Python, JavaScript, .NET and RE2-style engines, and a table of the differences that bite.
What a flavor is
A
regular expression
looks like a universal language, and the basics (literal characters,
., *, [a-z], ^)
do work almost everywhere. Beyond that, every tool and programming
language implements its own
regex flavor: a combination of
syntax (which characters are special and how features are written),
semantics (what \d, $ or \b
actually match) and an engine (how the match is found, and how
fast).
The same pattern can therefore be valid in one tool and an error in
another, or, worse, valid in both with different results. That
matters in daily IT work, because patterns travel: from a Stack
Overflow answer into a Python script, from a monitoring rule into a
grep command, from an AI assistant into a firewall
configuration.
The history behind the tree is on What Are Regular Expressions?. The practical point is the split between engines that backtrack (they try alternatives one by one and can be very slow on unlucky input) and engines that simulate an automaton (they never look at an input character more than a bounded number of times, but cannot offer every feature).
POSIX basic and extended syntax
POSIX, the Portable Operating System Interface standard for
Unix-like systems, defines two regex syntaxes. They are what
grep, sed and awk speak, and
they are introduced on
grep, find and regex.
A
POSIX basic regular expression
(BRE) is the older syntax, used by default in grep and
sed. In a BRE the characters ?,
+, {, |, ( and
) are ordinary characters; to get their special meaning
you put a backslash in front: \(…\),
\{2,3\}. A
POSIX extended regular expression
(ERE), used by grep -E, sed -E and
awk, works the other way round: those characters are
special as written, which is what people coming from programming
languages expect.
printf '%s\n' 'retry+ after 3s' 'retryyy 2' 'error|warn' 'warn disk 91%' > noc.log
grep -o 'retry+' noc.log # BRE: + is a literal plus
echo ---
grep -oE 'retry+' noc.log # ERE: + means "one or more y"
echo ---
grep 'error|warn' noc.log # BRE: | is a literal bar
echo ---
grep -E 'error|warn' noc.log # ERE: | is alternation
retry+ --- retry retryyy --- error|warn --- error|warn warn disk 91%
GNU grep and sed also accept
\+, \? and \| in basic syntax
as operators. That is a GNU extension: POSIX leaves it
implementation-defined, so on BSD or macOS the same command may
treat them as literal characters. When a script must run everywhere,
use -E and plain ERE.
Neither POSIX syntax has \d, lazy quantifiers,
lookaround or named groups. Instead POSIX defines bracket classes
such as [[:digit:]] and [[:space:]]. On
Debian and Ubuntu, GNU grep 3.8 does not even warn about
\d; it quietly treats it as the letter d:
echo 'MTR-0042 id=7' > meter.txt
grep -c -E 'id=\d' meter.txt # \d is just "d" here: no match
grep -c -E 'id=[0-9]' meter.txt
grep -c -E 'id=[[:digit:]]' meter.txt
grep -c -P 'id=\d' meter.txt # PCRE2 knows \d
0 1 1 1
The other big POSIX difference is not syntax but semantics. When several alternatives can match at the same position, POSIX requires the longest match; backtracking engines take the first alternative that works. See the comparison below and Alternation and Precedence.
PCRE and PCRE2
PCRE (Perl Compatible Regular Expressions) is a C library by Philip Hazel that implements Perl-style regular expressions for other programs. The original PCRE was released in 1997 and has reached end of life at version 8.45; its successor PCRE2 appeared in 2015 and is the one in use today (version 10.49, a security release from 28 September 2026, is the latest as of October 2026). When people say "PCRE" now, they nearly always mean PCRE2.
PCRE2 is the engine behind PHP's preg_* functions and
grep -P (since grep 3.8), and it is embedded in many
other programs. It is the most feature-rich of the common flavors:
lazy and possessive quantifiers, atomic groups, lookahead and
lookbehind, named groups in several syntaxes, \K,
recursion, Unicode properties (\p{…}; use the
u modifier for UTF-8 text) and inline flags.
Because it is a library, the features you get depend on the version your PHP or grep was built with. PCRE2 10.43 (February 2024) added variable-length lookbehind, as long as the length has a known maximum; PHP 8.2 on this site's test machine is linked to the system's PCRE2 10.42. An unbounded lookbehind fails in every version:
<?php
$log = 'user=amara id=7 user=hamid id=12';
// Fixed-length lookbehind: fine in every PCRE2 version
preg_match_all('/(?<=id=)\d+/', $log, $m);
echo implode(',', $m[0]), "\n";
// Unbounded lookbehind: rejected by every PCRE2 version
var_dump(preg_match('/(?<=user=\w+ id=)\d+/', $log)); // also prints a warning
// \K ("forget what matched so far") works in all versions
preg_match_all('/user=\w+ id=\K\d+/', $log, $m);
echo implode(',', $m[0]), "\n";
7,12 bool(false) 7,12
The failing call returns false and PHP prints a warning
("Compilation failed: lookbehind assertion is not fixed length").
Even PCRE2 10.43 and later reject this pattern: they allow
lookbehinds of different lengths, but only up to a known maximum
(255 characters by default), and \w+ has no maximum. A
bounded version such as
(?<=user=\w{1,32} id=) compiles from 10.43 onward.
\K works in every version. When a pattern behaves
differently on two servers, compare the library versions:
PCRE_VERSION in PHP, or the installed PCRE2 package (on
Debian, libpcre2-8-0) for grep -P.
Python's re module
Python's built-in re module has its own Perl-style
backtracking engine. Most patterns written for PCRE work unchanged,
but a few differences regularly catch people out:
-
Named groups are written
(?P<name>…). Python 3.11 rejects the(?<name>…)form that PCRE, JavaScript, Java and .NET use. -
In text (
str) patterns,\d,\wand\bare Unicode-aware by default.re.ASCIIrestricts them. -
\p{L}and other Unicode property escapes are not supported. The third-partyregexpackage (installed with pip) adds them, plus variable-length lookbehind and a timeout. - Lookbehind must have a fixed length. Possessive quantifiers and atomic groups exist since Python 3.11; older tutorials say they do not.
-
\Zmeans the absolute end of the text (what PCRE calls\z). Python 3.14 added\zas an alias; in Python 3.11 it is an error.
import re
print(re.findall(r"\d+", "MTR-0042 shows ٤٢"))
print(re.search(r"(?P<ticket>INC-\d+)", "see INC-31877")["ticket"])
print(re.match(r"a++a", "aaa")) # possessive: never gives a character back
for pattern in [r"(?<ticket>INC-\d+)", r"(?<=id=\d+)x", r"\p{L}"]:
try:
re.compile(pattern)
except re.error as error:
print("error:", error)
['0042', '٤٢'] INC-31877 None error: unknown extension ?<t at position 1 error: look-behind requires fixed-width pattern error: bad escape \p at position 0
JavaScript's RegExp
JavaScript regular expressions are defined by the ECMAScript
standard and implemented by each engine (V8 in Chrome and Node.js,
SpiderMonkey in Firefox, JavaScriptCore in Safari). The flavor has
grown a lot: lookbehind, named groups, the s flag and
Unicode property escapes arrived in ES2018, match indices in ES2022,
the v flag in ES2024, and modifier groups
(?i:…) and RegExp.escape() in ES2025. What
a given browser or Node.js version supports lags behind the
standard: RegExp.escape() arrived in Node.js 24, the
version that runs this site's examples, and Node.js 20 does not have
it.
-
\dand\ware ASCII only, even with theuflag (withiandutogether,\walso matches ſ and the Kelvin sign); use\p{Nd}or\p{L}withuorvfor other scripts. - Lookbehind may have any length, which Python and PCRE2 do not allow (PCRE2 10.43 and later accept only a bounded length).
-
There are no possessive quantifiers, no atomic groups, no verbose
xflag and no inline(?i). -
$without themflag matches only at the very end, not before a final newline.
console.log('MTR-0042 shows ٤٢'.match(/\d+/g));
console.log('MTR-0042 shows ٤٢'.match(/\p{Nd}+/gu));
console.log('user=amara id=7'.match(/(?<=user=\w+ id=)\d+/)[0]);
console.log(typeof RegExp.escape);
try {
new RegExp('a++a');
} catch (error) {
console.log(error.message);
}
[ '0042' ] [ '0042', '٤٢' ] 7 function Invalid regular expression: /a++a/: Nothing to repeat
.NET and Java
Both platforms ship their own backtracking engines in the standard
library (System.Text.RegularExpressions in .NET,
java.util.regex in Java), close to PCRE in syntax. They
are not run on this site, so here are only the points that matter
when a pattern moves to or from them:
-
Both write named groups as
(?<name>…), like PCRE and JavaScript. -
.NET has options found nowhere else:
RegexOptions.RightToLeftsearches from right to left, and since .NET 7RegexOptions.NonBacktrackingswitches to an engine that guarantees processing time linear in the input length. -
.NET can stop a runaway match with a timeout
(
RegexMatchTimeoutException), but the default is no timeout at all, so you have to set one. - Java is generally described as supporting possessive quantifiers, atomic groups and bounded lookbehind, without a built-in timeout; check the documentation of your Java version before relying on details.
RE2 and linear-time engines
RE2 is a regex library from Google
designed for untrusted patterns and input: it guarantees that
matching time grows linearly with the length of the input, whatever
the pattern. It does so by matching with an automaton instead of
backtracking, and by leaving out the features that need
backtracking. RE2's own syntax reference marks backreferences,
lookahead, lookbehind, possessive quantifiers and atomic groups as
not supported. It does support Unicode classes such as
\p{L} and both (?P<name>…) and
(?<name>…) for named groups.
The same approach is used by Go's standard
regexp package (RE2 syntax) and Rust's
regex crate, whose documentation promises worst-case
time of O(m × n) (pattern size times input size) and says it lacks
look-around and backreferences because nobody knows how to implement
them efficiently. ripgrep, a fast grep alternative,
uses the Rust engine by default. GNU grep's own matcher for patterns
without backreferences also avoids backtracking, which is why
grep -E rarely gets slow.
Why it matters: a backtracking engine can need an exponential number of steps on some pattern and input combinations. That is the basis of regular expression denial of service, explained on Performance and Catastrophic Backtracking. Cloudflare's 2019 outage report, for example, named a move to RE2 or the Rust engine as a follow-up. If you accept patterns from users, or run patterns on input from the internet, a linear-time engine removes that whole class of problems.
Differences that bite
The clearest difference is the matching rule itself. The pattern
x|xy|xyz on the text xyz can match
x, xy or xyz at the same
position. POSIX tools return the longest; backtracking engines
return the first alternative that succeeds:
echo 'xyz' > t.txt
grep -oE 'x|xy|xyz' t.txt
sed -E 's/x|xy|xyz/[&]/' t.txt
awk '{ sub(/x|xy|xyz/, "[&]"); print }' t.txt
grep -oP 'x|xy|xyz' t.txt
xyz [xyz] [xyz] x
The three POSIX tools agree on xyz;
grep -P stops at x. Below, the same four
checks run in Python, JavaScript and PHP: the alternation,
\d on an Arabic-Indic digit, $ before a
final newline, and the (?<name>…) syntax.
import re
print(re.search(r"x|xy|xyz", "xyz").group())
print(bool(re.search(r"\d", "٣")))
print(bool(re.search(r"abc$", "abc\n")))
try:
re.compile(r"(?<id>\d+)")
print("named group ok")
except re.error:
print("named group: error")
x True True named group: error
console.log('xyz'.match(/x|xy|xyz/)[0]);
console.log(/\d/.test('٣'));
console.log(/abc$/.test('abc\n'));
try {
new RegExp('(?<id>\\d+)');
console.log('named group ok');
} catch {
console.log('named group: error');
}
x false false named group ok
<?php
preg_match('/x|xy|xyz/', 'xyz', $m);
echo $m[0], "\n";
var_dump((bool) preg_match('/\d/', '٣'));
var_dump((bool) preg_match('/abc$/', "abc\n"));
echo preg_match('/(?<id>\d+)/', '7') === 1 ? "named group ok\n" : "named group: error\n";
x bool(false) bool(true) named group ok
The wider picture, as of 2026 and for the versions named:
| Feature | POSIX ERE (GNU) | PCRE2 10.42 (PHP 8.2) | Python 3.11 re |
JavaScript (Node.js 24) | RE2 / Go / Rust |
|---|---|---|---|---|---|
| Matching rule | leftmost-longest | leftmost-first | leftmost-first | leftmost-first | leftmost-first by default |
\d |
not a digit class | ASCII; Unicode with u |
Unicode digits | ASCII only | ASCII |
\p{L} |
no | yes | no | with u or v |
yes |
Lazy *? |
no | yes | yes | yes | yes |
| Possessive, atomic | no | yes | yes (3.11+) | no | no |
| Lookahead | no | yes | yes | yes | no |
| Lookbehind | no | fixed length per alternative (bounded variable length, max 255, from 10.43) | fixed length | any length | no |
| Backreferences | \1 (extension) |
yes | yes | yes | no |
| Named group | no |
(?<n>…), (?P<n>…)
|
(?P<n>…) only |
(?<n>…) |
(?P<n>…), (?<n>…)
|
$ before final \n |
per line anyway |
matches (D or \z to prevent)
|
matches (\Z to prevent) |
does not match | does not match |
Inline (?i) |
no | yes | yes, at the start | no | yes |
| Worst-case time | linear without backreferences | exponential (limited by a backtrack limit) | exponential | exponential | linear |
Moving a pattern between flavors
Most regex bugs in operations work are not wrong logic but the right pattern in the wrong engine. A short checklist when you copy a pattern from documentation, a colleague or an AI assistant:
-
Which engine runs it?
grepwithout options is BRE, a log shipper may use RE2 or PCRE2, a web form'spatternattribute uses JavaScript's flavor (see Forms and Input). Look it up in the tool's documentation. -
Translate the syntax. Escape or unescape
+ ? | ( ) {for BRE, replace\dby[0-9]in POSIX tools, convert named groups, and move inline flags to the call or the other way round. -
Check the semantics. Should
\dand\waccept non-ASCII characters? Does$allow a trailing newline? Does the order of alternatives matter? - Check the features. Lookbehind, backreferences and possessive quantifiers are not available everywhere, and some depend on the library version.
- Test in the real engine with lines that must match and lines that must not. Online testers such as regex101 let you pick a flavor, but the final test belongs in the tool itself; see Testing, Debugging and Readability.