Skip to content

Add Regex+ engines with rr as default to close #854 - #855

Merged
mathiasrw merged 13 commits into
mainfrom
fix/854
Oct 27, 2026
Merged

mathiasrw merged 13 commits into
mainfrom
fix/854

Conversation

@mathiasrw

@mathiasrw mathiasrw commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Implements #500

Closes #854
Also closes #858

Change

Regex+ by Steven Levithan is now behind the engines of -E:

Engine What it is
rr (the default, also default) Reads patterns like the native engine and adds the extended syntax of Regex+: atomic groups (?>…), possessive quantifiers like a++, and subroutines \g<name> with (?(DEFINE)…). Only the transforms of Regex+ run, not its flags.
regex+ Regex+ with the flags v, x (free spacing) and n (only named groups capture). On a runtime without v, like Node 18, it uses u.
runtime The native engine of the runtime: V8 in Node and JavaScriptCore in Bun.
> echo aab | rexreplace '(?>a+)(b)' '[$1]'
[b]
> echo 3.12.0 | rexreplace '\b(?<v>\g<n>\.\g<n>\.\g<n>)(?(DEFINE)(?<n>0|[1-9]\d*))' 'v$<v>'
v3.12.0
  • Atomic groups and possessive quantifiers add a group of their own inside the regex. $1 in the replacement, in --js code and in --output-match still counts only the groups of the user. One exception: a subroutine copies the unnamed groups inside the group it calls, and those copies get numbers, as upstream does not hide them without a backreference. The README says so. --debug and --status with --verbose tell the engine and the added groups.
  • --literal gives the text as it is with every engine, also with the free spacing of regex+.
  • --exclude-re uses the same engine and flags as the pattern, so all regex in a run work the same way.
  • A transform of Regex+ only runs when its syntax is outside a character class, so [(?>]\1 is the same as in the native engine (Fix [(?>]\1 stopping the run with the rr engine #858).

--flags

--flags sets the flags of the regex, like gimsv, in place of the defaults: gim, or gimvxn with -E regex+. So -E regex+ --flags gimvx is Regex+ where all groups capture. It can not be used with -G, -I, -M, -s and -u, x and n need -E regex+, an unknown flag stops the run with the flag named, a letter given twice counts once, --flags given twice takes the last one, and u with v stops the run with a message. With the flag y, each file, each name of --rename and each path for --exclude-re starts from the beginning, and not where the one before ended.

Named groups in --js

A named group is $ and the name: (?<year>…) gives $year, and $year_ has a space at the end. The groups come after the other variables, so a group named like a number, like n, wins over $n. A name may end with _. With the flag n only named groups capture, so --js has $0 and the names, but no $1. (#863 makes the order of a group like a_ and the cousin of a not matter, and says that a replacement text still reads $1.)

For the release note of v8

  • Every piece of extended syntax was an error in the native engines, except \g<name>. So a pattern that works today gives the same regex with rr. The one change: \g<name> was the text g<name>, and with rr it calls the group. -E runtime keeps the old meaning.
  • -E V8 and -E JSC are gone. -E runtime is the native engine.
  • The named groups of --js are $year, not _year.

Good to know: Regex+ emulates atomic groups and possessive quantifiers with a lookahead and a backreference, and a lookbehind reads from right to left, so inside a lookbehind they look forward in place of back and match in the wrong place, with no error. -E regex+ -u uses u in place of v. A subroutine needs a named group that exists, as Regex+ has no numbered ones like \g<1>. Both are in the README, and so are two limits: without -u, a [ inside a class, like [[], hides the extended syntax after it (write [\[]), and counting the groups Regex+ adds, a replacement text can only reach the first 99 groups.

Regex+ as a dependency

regex 6.1.0 and regex-utilities 2.3.0 are dependencies. The build bundles them, as regex/internals and regex-utilities are ES modules only, and the build of the CLI is CommonJS. #862 builds RexReplace as an ES module, so they can be imported when it runs.

The subroutines of regex are not exported, so src/libs has subroutines.js and utils-internals.js as they are, and the three parts of utils.js they use: 429 lines, with no changes. RexReplace runs the subroutines, the possessive quantifiers and the atomic groups itself, to know which groups they add. For regex+, rewrite() runs with those turned off first. 65 patterns, with each of the flags v, x and n on and off, give the same regex and the same added groups as a patched rewrite().

Bundle Minified
main 52 KB 35 KB
This pull request 108 KB 60 KB

regex and regex/internals are two builds in the package, so the part both use is bundled twice. #862 takes the size down. When upstream exports subroutines, the copy goes, and src/libs/README.md says how.

Tests

test/fix/854.test.js checks:

  • rr gives the same regex as the native engine for 26 legacy patterns, like \", a], \01, [a++], [(?>]\1 and [\g<x>].
  • rr reads atomic groups, possessive quantifiers, subroutines and DEFINE, and default is rr.
  • The added groups are not counted in $1, $2$1, $3 (stays text), $10, --js and --output-match.
  • runtime reads \g<d> as text. regex+ has free spacing, only named groups capture, and the v flag. V, V8, JSC, regex+n, regex+x, regex+u and regex+v are no engines, with the message checked. regex+ stops the run on \", and --literal with regex+ finds a b #c (d) as it is.
  • --flags: leaving out x or n, all flags set, u in place of v, v when neither is given, a flag twice, --flags with -I in the CLI, and the message for x or n without regex+ and for an unknown flag.
  • --js with n has $0 and the named groups, and $1 is not defined. Without n the numbers are back.

The tests of #799, #815, #816 and #825 use $year now. yarn test-ci passes.

@mathiasrw
mathiasrw marked this pull request as draft October 24, 2026 21:41
@mathiasrw
mathiasrw marked this pull request as ready for review October 24, 2026 21:45
@mathiasrw
mathiasrw marked this pull request as draft October 24, 2026 22:21
@mathiasrw
mathiasrw marked this pull request as ready for review October 24, 2026 22:27
@mathiasrw
mathiasrw marked this pull request as ready for review October 25, 2026 22:30
…outine copies, the lookbehind and -u with regex+
@mathiasrw
mathiasrw added this pull request to stack #864 October 27, 2026 13:33
@mathiasrw
mathiasrw merged commit d90b720 into main Oct 27, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix [(?>]\1 stopping the run with the rr engine Add Regex+ engines, with rr as the default engine

1 participant