Skip to content

Portability between engines

One line: A pattern moving from one engine to another fails in one of three ways — it refuses to compile (the good one), it compiles and means something else (the bad one), or it compiles, means the same thing, and matches differently because \w or $ or (?i) is defined differently underneath — and this page is the checklist for all three, with a table that shows twelve constructs doing each.

Level: 301 · you maintain the same pattern in two languages, or a pattern in a config file that more than one program reads

The three failure modes

Mode Example You find out
Refused (?P<n>…) in JavaScript; \p{L} in Python at compile time, with a message
Compiles, means something else a++ in Rust; \Q in Perl-from-a-string; {,2} in JavaScript; (?R) in Rust when a test happens to hit the difference — often never
Same construct, different definition \w on é; $ on a\n; (?i) on U+212A in production, on someone else's data

The first is a compile error and costs five minutes. The second and third are the reason this library exists.

Measured

Verified output of topic_portability_between_engines_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

construct                   python  perl     node  ruby     java     go       rust     fancy
(?P<n>…) Python-style name  a       a        -     -        -        a        a        a
(?<n>…) everyone else       -       a        a     a        a        a        a        a
\z true end                 -       a        no    a        a        a        a        a
\Z, two meanings            no      a        no    a        a        -        -        a
\p{L}                       -       \u{3B1}  no    \u{3B1}  \u{3B1}  \u{3B1}  \u{3B1}  \u{3B1}
\h horizontal space         -       \t       no    no       \t       -        -        no
a++ possessive              no      no       -     no       no       -        aa       no
(?>…) atomic                no      no       -     no       no       -        -        no
(?<=a|bc) two-width         -       d        d     d        d        -        -        d
{,2} open lower bound       aa      aa       no    aa       -        no       -        aa
(?R) recursion              -       (a(b))   -     -        -        -        (b)      -
\Q…\E                       -       no       no    no       a.b      a.b      -        -

Reading the table

  • Rows 1–2, naming a group. No spelling that all eight accept. (?P<n>…) is Python, Perl, Go, Rust; (?<n>…) is everyone but Python. PCRE2 — ABAP — takes both.
  • Rows 3–4, the end of the subject. \z is missing from Python 3.12 (3.14 added it) and is the letter z in JavaScript. \Z steps over a final newline in Perl, Ruby, Java and fancy-regex, does not in Python, and is refused by Go and Rust. Two escapes, four behaviours.
  • Rows 5–6, escapes that are letters somewhere. \p{L} is refused by Python and is the text p{L} in JavaScript without u. \h is horizontal space in Perl and Java, a hex digit in Ruby, the letter h in JavaScript, and refused by the rest.
  • Rows 7–8, possessive and atomic. Both do the same job; both are refused by JavaScript and Go; and a++ in Rust's regex is (a+)+, which matches where the possessive form cannot.
  • Row 9, lookbehind width. Two alternatives of different lengths: fine in Perl, JavaScript, Ruby and Java; refused by Python and fancy-regex.
  • Row 10, {,2}. A quantifier in four engines, refused in two, and the literal text {,2} in JavaScript and Go.
  • Row 11, (?R). Recursion in Perl; a CRLF flag in Rust, which then matches the inner (b).
  • Row 12, \Q…\E. In the engine in Java and Go; in the string in Perl, so refused-or-literal when it arrives at the engine as text.

The checklist

Before a pattern crosses an engine boundary, look for these, in roughly the order they bite:

  1. Anchors. ^…$ → \A…\z (or the language's fullmatch), and check what \Z means on the far side. The $ page.
  2. Shorthands. \w, \d, \s, \b — ASCII or Unicode on each side? If it matters, spell it out with \p{…} or a class, and check \p{…} exists. Shorthand classes.
  3. Group names and the references to them, in the pattern and in the replacement. Named group · Replacement references.
  4. Flags. m and s and x, and whether they can be inline. Inline flags.
  5. Anything after (? that is not :, =, !, <=, <!, <name>. Atomic, conditional, recursion, branch reset, verbs: each has a page and a column of -.
  6. Lazy quantifiers heading for a POSIX tool. sed, awk and grep -E have none. Greedy and lazy.
  7. Alternation order heading for or from a POSIX tool. Leftmost-first and leftmost-longest.
  8. (?i) on non-ASCII text. Case-insensitive matching.

And one process rule: test the pattern in the engine that will run it, with the inputs that differ. A pattern debugged on regex101 in the wrong flavour is debugged against the wrong engine.

The lineages, as a prediction

Four families, and knowing which one an engine belongs to predicts most of the table above: Perl/PCRE2 (Perl, PCRE2, ABAP, PHP), the middle ground (Java, Ruby, JavaScript, .NET — each missing a different corner), Python's re (strict about lookbehind and forward references, none of \p), and the automata (Go, Rust's regex, RE2 — no backreferences, no lookaround, a linear-time guarantee). Who supports what is the measured version.

Keywords on this page

Every keyword page has a column of - cells, and that column is this topic. Start with named group, \A, \z, \Z, \p{…}, possessive quantifier, {n,m}, recursion, \Q…\E.

See also