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.

Simplified family tree of regex flavors: the POSIX line from ed and grep, the Perl-style backtracking line leading to PCRE, Python, JavaScript, Java and .NET, and the linear-time line leading to RE2, Go and Rust Regular sets in theory: Kleene, 1951 Thompson: QED, ed, grep; later sed, awk Backtracking libraries (Spencer, 1980s), Perl Automaton matching (Thompson, 1968) POSIX BRE and ERE standard syntax PCRE, PCRE2 1997, 2015 Perl-style engines own implementations RE2 linear time grep, sed, gawk PHP, grep -P Python, JS, Java, .NET Go, Rust, ripgrep Simplified: lines show influence, not shared source code.
Three families. The POSIX tools and the linear-time engines both descend from automaton-based matching; the Perl-style engines backtrack, which makes features such as backreferences easy and worst-case performance poor.

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, \w and \b are Unicode-aware by default. re.ASCII restricts them.
  • \p{L} and other Unicode property escapes are not supported. The third-party regex package (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.
  • \Z means the absolute end of the text (what PCRE calls \z). Python 3.14 added \z as 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.

  • \d and \w are ASCII only, even with the u flag (with i and u together, \w also matches ſ and the Kelvin sign); use \p{Nd} or \p{L} with u or v for 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 x flag and no inline (?i).
  • $ without the m flag 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.RightToLeft searches from right to left, and since .NET 7 RegexOptions.NonBacktracking switches 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:

  1. Which engine runs it? grep without options is BRE, a log shipper may use RE2 or PCRE2, a web form's pattern attribute uses JavaScript's flavor (see Forms and Input). Look it up in the tool's documentation.
  2. Translate the syntax. Escape or unescape + ? | ( ) { for BRE, replace \d by [0-9] in POSIX tools, convert named groups, and move inline flags to the call or the other way round.
  3. Check the semantics. Should \d and \w accept non-ASCII characters? Does $ allow a trailing newline? Does the order of alternatives matter?
  4. Check the features. Lookbehind, backreferences and possessive quantifiers are not available everywhere, and some depend on the library version.
  5. 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.