Skip to content

Matching nested structures

One line: Balanced parentheses are not a regular language, so an engine either ships an explicit recursion construct or genuinely cannot express them — and the construct has two incompatible spellings: (?R) / (?1) / (?&name) in Perl and PCRE2, \g<0> / \g<1> / \g<name> in Ruby.

Level: 301 · you are about to match something nested with a regex, or about to decide not to

Keywords on this page: (?R) · \1 · (?<name>…) · (?(1)…) — Topics: Choosing an engine · Readable patterns

The wall, one level up

What a backreference costs is about the first wall: (\w+) \1 is not a regular language, so an engine that offers it must abandon the finite automaton and search. Nesting is the same wall a level higher. A finite automaton has finitely many states, and "how many parentheses are still open" is an unbounded counter — for every automaton you can write down a depth it cannot count to. That is not a limitation of any implementation; it is what regular means.

So there are exactly three honest positions an engine can take, and every engine on this page takes one of them: add a construct that lets a pattern call itself, refuse the whole category, or let you unroll the nesting to a fixed depth and quietly get the wrong answer one level further down.

Six spellings, five engines

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

does it COMPILE?           python  perl   node   ruby   java  
(?R)                       no      yes    no     no     no    
(?1)                       no      yes    no     no     no    
(?&p), with (?<p>...)      no      yes    no     no     no    
(?&p), with (?P<p>...)     no      yes    no     no     no    
\g<0>                      no      no     yes    yes    no    
\g<p>, with (?<p>...)      no      no     yes    yes    no    

does that engine's OWN anchored recursive pattern MATCH?
subject      python  perl   node   ruby   java  
((a)(b))     n/a     yes    no     yes    n/a   
(ab)         n/a     yes    yes    yes    n/a   
(a(b)c       n/a     no     no     no     n/a   

in node, is \g<0> recursion or the literal text g<0> ?
subject      with \g<0>   with literal g<0>
((a)(b))     no           no
(g<0>)       yes          yes
(ab)         yes          yes
under the u flag, node rejects \g<0> outright: refused

What to notice

There are two dialects, and they do not overlap at all. Perl compiles (?R), (?1) and (?&name) and refuses \g<0>. Ruby compiles \g<0> and \g<name> and refuses all three of Perl's. Not one spelling works in both. A pattern that recurses is therefore not portable between the two largest families that have recursion, never mind the ones that do not.

Ruby has recursion. Who supports what says recursion is "Perl's alone among these five", and that is wrong — its probe asks every engine for (?R), which is the Perl spelling, and reads Ruby's refusal as an absence. Ruby's engine is Onigmo, which took subroutine calls from a different branch of the family and spells them \g<…>. Ask in Onigmo's spelling and Ruby recurses exactly as Perl does — the second table here, and the Ruby section below line for line. The (?R) spelling is what fails in Ruby, not recursion.

Node compiled \g<0> and it is not recursion. Outside unicode mode, JavaScript treats \g as an identity escape: \g<0> is the four literal characters g<0>. The proof is the last table — replacing \g<0> with a plain g<0> changes nothing, and the pattern matches the subject (g<0>) while failing ((a)(b)). Turn on the u flag and the same pattern is rejected outright, which is the kinder answer. This is the sharpest example in this library of why a "yes" in a compile table means only that engine compiled that pattern.

Perl, where the construct comes from

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

(?R), unanchored
  (a(b)c)    found '(a(b)c)'
  ((a)(b))   found '((a)(b))'
  ()         found '()'
  (a(b)c     found '(b)'
  a          no match

^...(?R)...$ -- the anchors are INSIDE the recursion
  (a(b)c)    no match
  ((a)(b))   no match
  ()         match
  (a(b)c     no match
  a          no match

^((?1) form)$ -- whole subject must be one balanced group
  (a(b)c)    match
  ((a)(b))   match
  ()         match
  (a(b)c     no match
  a          no match

^((?&par) form)$ -- the same thing, named
  (a(b)c)    match
  ((a)(b))   match
  ()         match
  (a(b)c     no match
  a          no match

subroutine call vs backreference
  12-34    (?1) yes  \1 no
  12-12    (?1) yes  \1 yes

nesting depth, with the (?1) form
  depth 1     match
  depth 10    match
  depth 100   match
  depth 1000  match

A subroutine call is not a backreference

(?1) re-runs group 1's pattern. \1 re-matches group 1's text. The 12-34 line above is the whole distinction in one row: (\d\d)-(?1) accepts 12-34 and 12-12, while (\d\d)-\1 accepts only 12-12. The forward references page draws that line in its table of four names and adds the conditional (?(1)yes|no); this page is what you do once you have it.

Recursion is just the case where the group calls itself. (\((?:[^()]|(?1))*\)) reads: a (, then any number of either a non-paren character or another one of me, then a ).

The trap: (?R) includes your anchors

(?R) means "re-run the whole pattern", and the whole pattern is everything you wrote — anchors included. Wrap it as ^\((?:[^()]|(?R))*\)$ and the recursive call tries to match ^ in the middle of the subject, which cannot succeed. The pattern compiles. It matches () and (ab) perfectly well, because those never enter the recursion, and then silently fails on everything nested. That is the worst possible failure mode: it looks like it works.

The fix is to recurse into a group rather than the whole pattern — ^(\((?:[^()]|(?1))*\))$ — so the anchors stay outside the part being called. Ruby has the identical trap with \g<0>, and the identical fix with \g<1>.

Ruby, in the other dialect

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

does it COMPILE?
  (?R)     no
  (?1)     no
  (?&par)  no
  \g<0>    yes
  \g<1>    yes
  \g<par>  yes

\g<0>, unanchored
  (a(b)c)    found '(a(b)c)'
  ((a)(b))   found '((a)(b))'
  ()         found '()'
  (a(b)c     found '(b)'
  a          no match

\A...\g<0>...\z -- the anchors are INSIDE the recursion
  (a(b)c)    no match
  ((a)(b))   no match
  ()         match
  (a(b)c     no match
  a          no match

\g<1> and \g<par>, anchored
  (a(b)c)    by number match     by name match
  ((a)(b))   by number match     by name match
  ()         by number match     by name match
  (a(b)c     by number no match  by name no match
  a          by number no match  by name no match

nesting depth, with the \g<1> form
  depth 1     match
  depth 10    match
  depth 100   match
  depth 1000  match

Line for line the same answers as Perl, including the anchor trap: \A…\g<0>…\z matches () and nothing nested, and \g<1> or \g<par> is what you wanted. The only thing Ruby does differently is how you spell it.

ABAP: the one place it beats Python

ABAP's PCRE addition (7.55 and later) hands the pattern to the PCRE2 library in the kernel, so ABAP gets Perl's recursion in full. This matters more than it sounds: PCRE2 recursion is one of the few places where a plain ABAP one-liner does something Python's standard library cannot do at all.

z_nested_parens.prog.abap in full — pasted here by tools/run_examples.py from the file CI runs.

REPORT z_nested_parens.

" Nested structures in ABAP, in the PCRE syntax available as of release 7.55.
" The PCRE addition hands the pattern to the PCRE2 library in the kernel, so
" ABAP gets Perl's recursion: (?R), (?1) and (?&name). Python, Java, JavaScript,
" Go and Rust have none of these, which makes this one of the few places where
" an ABAP one-liner does something those languages need a loop for.
"
" Nothing in this repository can run ABAP, so this file is checked by abaplint
" and printed as source only -- there is no recorded output beside it. What the
" engine underneath does is on the lesson page, as a dated pcre2grep run.

DATA candidates TYPE STANDARD TABLE OF string WITH EMPTY KEY.
DATA verdict TYPE string.

DATA(text) = `f( a, g( b, h( c ) ), d ) + k( e )`.

" (?1) re-runs group 1's PATTERN at this position -- not the text group 1
" captured, which is what \1 would mean. That is the whole trick: the group's
" definition mentions the group, so it can nest to any depth.
DATA(outermost) = `(\((?:[^()]|(?1))*\))`.

FIND ALL OCCURRENCES OF PCRE outermost IN text RESULTS DATA(matches).

LOOP AT matches INTO DATA(match).
  WRITE / |{ match-offset } { substring( val = text
                                         off = match-offset
                                         len = match-length ) }|.
ENDLOOP.

" Anchoring needs the (?1) form, not (?R). ^...(?R)...$ compiles and then
" matches only subjects that never enter the recursion, because the recursive
" call re-runs the anchors too.
DATA(whole_string) = `^(\((?:[^()]|(?1))*\))$`.

APPEND `(a(b)c)` TO candidates.
APPEND `(a(b)c` TO candidates.
APPEND `a)b` TO candidates.

LOOP AT candidates INTO DATA(candidate).
  FIND FIRST OCCURRENCE OF PCRE whole_string IN candidate.
  IF sy-subrc = 0.
    verdict = `balanced`.
  ELSE.
    verdict = `not one balanced group`.
  ENDIF.
  WRITE / |{ candidate } { verdict }|.
ENDLOOP.

" Worth saying out loud: a recursive pattern is still a backtracking search, so
" hostile input costs what backtracking costs. When the structure is a real
" grammar -- nested IF blocks, XML, a formula language -- count the depth in
" ABAP instead, or parse it. The regex is shorter, not safer.

Nothing in this repository can run ABAP. What can be run is the engine underneath, and that is pcre2grep — the same PCRE2 library, asked the same questions:

Real run — PCRE2 10.48 (pcre2grep version 10.48 2026-08-31), macOS 26.6.2 x86-64, 2026-09-22.

$ cat nested.txt
f(a, g(b, h(c)), d)
(a(b)c
((a)(b))

$ pcre2grep -n -o '(\((?:[^()]|(?1))*\))' nested.txt
1:(a, g(b, h(c)), d)
2:(b)
3:((a)(b))

$ pcre2grep -n -o '(?<par>\((?:[^()]|(?&par))*\))' nested.txt
1:(a, g(b, h(c)), d)
2:(b)
3:((a)(b))

$ pcre2grep -n '^(\((?:[^()]|(?1))*\))$' nested.txt
3:((a)(b))

$ pcre2grep -n '^\((?:[^()]|(?R))*\)$' nested.txt ; echo "exit $?"
exit 1

Three things in that transcript are worth pausing on. (?1) and (?&par) give identical results, so naming the group costs nothing and survives someone inserting a group ahead of it — in ABAP source, prefer the name. The unanchored form found (b) on line 2, inside the unbalanced input (a(b)c: a recursive pattern reports the balanced run it found, and says nothing at all about the text around it. And the last command, the (?R) form with anchors, matched no line of a file whose third line is perfectly balanced — the trap above, on real PCRE2.

The engines that cannot

Python re has no recursion in any spelling; the probe's python column is no six times, including for (?&p) written with Python's own (?P<p>…) group syntax. The third-party regex module does implement (?R) and (?&name); nothing on this page measures it, because every Python example in this library runs under python3 -I with the standard library only.

JavaScript has none. \g<0> compiling is the identity-escape accident above, not a feature.

Java refuses all six spellings.

.NET is the other family's answer: balancing groups, (?<-open>…) with (?(open)(?!)), which count with a capture stack instead of recursing. It solves the same problem by a different route and is worth knowing exists. Not measured here — nothing in this repository runs .NET.

Go: a fixed depth, and then a silent wrong answer

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

does it COMPILE?
  (?R)     false
  (?1)     false
  (?&p)    false
  \g<0>    false

unrolled to depth 4, 55 characters of pattern:
  \((?:[^()]|\((?:[^()]|\((?:[^()]|\([^()]*\))*\))*\))*\)

how deep does it reach?
  depth 1  (a)            true
  depth 2  ((a))          true
  depth 3  (((a)))        true
  depth 4  ((((a))))      true
  depth 5  (((((a)))))    false
  depth 6  ((((((a))))))  false

the pattern cannot be written to reach every depth: each level
is another copy of the one below it, and there is no last level.

Go's regexp is RE2, so all four spellings are refused at compile time — which is the honest answer, and the loud one. The second half is what people reach for instead: unroll the nesting by hand. Four levels of unrolling is already 55 characters of pattern, it grows by a constant factor per level, and at depth 5 it returns false with no error and no warning. There is no value of the limit that is correct, because the limit is what the automaton's finiteness forces on you.

Rust: one crate refuses, one accepts (?R) and means something else

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

(?R)
  regex:       COMPILED
  fancy-regex: refused -- Parsing error at position 14: Unknown group flag: (?R
(?1)
  regex:       refused -- regex parse error: ^(\((?:[^()]|(?1))*\))$ ^ error: unrecognized flag
  fancy-regex: refused -- Parsing error at position 15: Unknown group flag: (?1
\g<0>
  regex:       refused -- regex parse error: ^\((?:[^()]|\g<0>)*\)$ ^^ error: unrecognized escape sequence
  fancy-regex: refused -- Error compiling regex: Regex uses currently unimplemented feature: Subroutine Call

what regex's (?R) pattern actually matches
  (a(b)c)    false
  ((a)(b))   false
  (ab)       true
  ()         true
  a          false

fancy-regex refuses all three, and its message for \g<0> — Regex uses currently unimplemented feature: Subroutine Call — shows it parses the Onigmo spelling and declines to run it.

regex is the interesting one. It compiled ^\((?:[^()]|(?R))*\)$, because R is regex's flag for CRLF line terminators: (?R) is a flag-setting group, it consumes nothing, and the alternation branch that was supposed to recurse is empty. The pattern collapses to "one level of parentheses" and matches (ab) and () while rejecting (a(b)c) and ((a)(b)). Nothing warns. This is the same failure shape as node's \g<0>, from a completely different cause, and it is the reason a compile-time "yes" is never the end of the question.

The portable answer is one counter

balanced_by_counting_py.py in full — pasted here by tools/run_examples.py from the file CI runs.

"""The portable answer: count the depth in the host language.

Python's `re` has no recursion in any spelling, so this file also shows what it
does with the two dialects -- and then does the job with one counter and one
pass, in a shape that works in every language on the page, needs no engine
feature, and says *where* the input went wrong instead of only that it did.
"""

import re


def scan(text):
    """Report the first balanced parenthesised group in `text`, or what broke."""
    depth = 0
    start = None
    for offset, char in enumerate(text):
        if char == "(":
            if depth == 0:
                start = offset
            depth += 1
        elif char == ")":
            if depth == 0:
                return f"unexpected ')' at offset {offset}"
            depth -= 1
            if depth == 0:
                return f"balanced, offsets {start}..{offset}"
    if depth:
        return f"{depth} unclosed '(', outermost at offset {start}"
    return "no parentheses"


SUBJECTS = ["(a(b)c)", "((a)(b))", "()", "(a(b)c", "a)b", "a"]

print("what Python's re does with a recursive pattern")
for label, pattern in [
    ("(?R)", r"\((?:[^()]|(?R))*\)"),
    ("(?1)", r"(\((?:[^()]|(?1))*\))"),
    ("(?&p)", r"(?P<p>\((?:[^()]|(?&p))*\))"),
    (r"\g<0>", r"\((?:[^()]|\g<0>)*\)"),
]:
    try:
        re.compile(pattern)
        outcome = "compiled"
    except re.error:
        outcome = "refused at compile time"
    print(f"  {label:<8} {outcome}")

print("\ncounting instead")
for subject in SUBJECTS:
    print(f"  {subject:<10} {scan(subject)}")

print("\nno engine limit applies: depth is just an integer")
for depth in (10, 1000, 100_000):
    print(f"  depth {depth:<7} {scan('(' * depth + 'a' + ')' * depth)}")

print("\nand hostile input is answered in one pass, not searched")
print(f"  {'(' * 40:.12}...  {scan('(' * 40)}")

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

what Python's re does with a recursive pattern
  (?R)     refused at compile time
  (?1)     refused at compile time
  (?&p)    refused at compile time
  \g<0>    refused at compile time

counting instead
  (a(b)c)    balanced, offsets 0..6
  ((a)(b))   balanced, offsets 0..7
  ()         balanced, offsets 0..1
  (a(b)c     1 unclosed '(', outermost at offset 0
  a)b        unexpected ')' at offset 1
  a          no parentheses

no engine limit applies: depth is just an integer
  depth 10      balanced, offsets 0..20
  depth 1000    balanced, offsets 0..2000
  depth 100000  balanced, offsets 0..200000

and hostile input is answered in one pass, not searched
  ((((((((((((...  40 unclosed '(', outermost at offset 0

One integer, one pass. That runs in every language on this page, needs no engine feature, and — the part no recursive pattern can match — it tells you where the input went wrong. 1 unclosed '(', outermost at offset 0 and unexpected ')' at offset 1 are answers; no match is not. Depth 100000 is fine, because the depth is an integer rather than an engine's recursion budget, and the forty-open-paren hostile input is answered in one pass instead of searched.

Compare that against the recursive pattern on the same unbalanced input: unanchored, PCRE2 reported (b), and anchored it reported nothing. Neither told you which parenthesis was the problem, and the unanchored one looks like success.

What it costs

A recursive pattern is still a backtracking search, so hostile input costs what backtracking costs. PCRE2 does not let it run forever — it counts, and gives up:

Real run — PCRE2 10.48, macOS 26.6.2 x86-64, 2026-09-22. deep30.txt holds thirty (, an a, and thirty ).

$ pcre2grep --match-limit=50 -c '^(\((?:[^()]|(?1))*\))$' deep30.txt
pcre2grep: pcre2_match() gave error -47 while matching this text:

((((((((((((((((((((((((((((((a))))))))))))))))))))))))))))))

pcre2grep: Error -46, -47, -53 or -63 means that a resource limit was exceeded.
pcre2grep: Check your regex for nested unlimited loops.
0

$ pcre2grep --match-limit=100 -c '^(\((?:[^()]|(?1))*\))$' deep30.txt
1

Fifty units of match budget is not enough for depth 30; a hundred is. The limit is a runtime resource, not a property of the pattern, which means a pattern that passes your tests can fail in production on deeper input — and the failure arrives as an engine error, not as "no match". Everything on catastrophic backtracking applies here, with recursion supplying yet another way to nest unbounded loops.

The measured summary

Engine Perl spelling (?R) (?1) (?&p) Onigmo spelling \g<0> \g<1> Actually recurses?
Perl compiles rejected yes
PCRE2 / ABAP PCRE compiles not asked here yes — real pcre2grep run above
Ruby rejected compiles yes
JavaScript (node) rejected compiles — as literal text no
Python re rejected rejected no
Java rejected rejected no
Go regexp rejected rejected no, by design
Rust regex (?R) compiles — as a CRLF flag rejected no
Rust fancy-regex rejected rejected — "unimplemented" no
.NET — — balancing groups instead; not measured here

Every cell above except the last row came from a program in this folder or from the dated transcript above.

So should you use it?

Rarely, and the reasons are not about capability.

Nesting deep enough to need recursion is nesting deep enough to be a grammar, and a grammar wants a parser. A parser gives you the position of the error, a partial result, and a structure you can walk; a recursive pattern gives you a boolean and a span. The scan above is the floor of what a parser costs, and it already beats the regex on the question users actually ask, which is what is wrong with my input.

Where it does earn its place: a one-off extraction over trusted input, in a codebase that is already committed to one engine — a pcre2grep invocation, an ABAP report pulling nested groups out of a fixed-format field. Even there, use (?&name) rather than (?1) so the pattern survives an edit, keep the anchors outside the recursion, and write down next to it which engine it is for. A recursive pattern is portable to exactly one of the two dialects, invisible to the reader who has not met the construct, and — as both node and Rust's regex demonstrate on this page — capable of compiling in an engine that will do something else entirely with it.

See also