Skip to content

$1, \1, ${name}, $& — the replacement string is a second language

One line: The string on the right-hand side of a substitution has its own tiny grammar for "insert what group 1 matched", and there is no spelling that works in all eight engines: $1 in Perl, JavaScript, Java, Go and Rust; \1 in Python and Ruby; and the same divergence again for names, for the whole match, and even for a literal dollar sign.

Level: reference · one construct, eight engines

What it means

A replacement string is not a pattern. It is a template, and each engine wrote its own:

Engine Group by number Group by name Whole match Literal $
Python \1, \g<1> \g<name> \g<0> $ (just type it)
Perl $1, ${1} $+{name} $& \$
JavaScript $1 $<name> $& $$
Ruby \1 \k<name> \0, \& $
Java $1 ${name} $0 \$
Go $1, ${1} ${name} $0, ${0} $$
Rust regex $1, ${1} ${name} $0, ${0} $$
PCRE2 / ABAP $1, ${1} ${name} $0 $$

Every cell that is not listed for an engine is either a runtime error (Java, mostly) or silently the literal text (everyone else, mostly). Perl's column is Perl string interpolation, which means $& is a real variable and \1 in a double-quoted string is a control character.

Measured

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

construct          python    perl        node       ruby       java     go         rust       fancy
$1                 $2$1      ba          ba         $2$1       ba       ba         ba         ba
\1                 ba        \u{2}\u{1}  \\2\\1     ba         21       \\2\\1     \\2\\1     \\2\\1
${1}               ${2}${1}  ba          ${2}${1}   ${2}${1}   err      ba         ba         ba
$<name>            -         n/a         [a]        [$<w>]     err      [$<w>]     [$<w>]     [$<w>]
${name}            -         n/a         [${w}]     [${w}]     [a]      [a]        [a]        [a]
\k<name>           -         [k<w>]      [\\k<w>]   [a]        [k<w>]   [\\k<w>]   [\\k<w>]   [\\k<w>]
\g<name>           -         [g<w>]      [\\g<w>]   [\\g<w>]   [g<w>]   [\\g<w>]   [\\g<w>]   [\\g<w>]
$+{name} (Perl)    -         [a]         [$+{w}]    [$+{w}]    err      [$+{w}]    [$+{w}]    [$+{w}]
whole match $&     x[$&]     x[a]        x[a]       x[$&]      err      x[$&]      x[$&]      x[$&]
whole match $0     x[$0]     n/a         x[$0]      x[$0]      x[a]     x[a]       x[a]       x[a]
whole match \0     x[\u{0}]  x[\u{0}]    x[\\0]     x[a]       x[0]     x[\\0]     x[\\0]     x[\\0]
whole match \g<0>  x[a]      x[g<0>]     x[\\g<0>]  x[\\g<0>]  x[g<0>]  x[\\g<0>]  x[\\g<0>]  x[\\g<0>]
literal dollar $$  $$        n/a         $          $$         err      $          $          $
literal dollar \$  \\$       $           \\$        \\$        $        \\$        \\$        \\$

Reading the table

  • Rows 1–2, $1 versus \1. The split, exactly as advertised: $2$1 gives ba in five engines and is left as text in Python and Ruby; \2\1 gives ba in Python and Ruby, becomes two control characters in Perl (octal escapes in a string), the digits 21 in Java (backslash quotes the next character), and stays as the four characters \2\1 in JavaScript, Go and Rust. No error anywhere.
  • Row 3, ${1}. Braces are Perl, Go and Rust. Java refuses ${2} at runtime — its braces are for names only.
  • Rows 4–8, names. $<w> is JavaScript's and nobody else's; ${w} is Java, Go and Rust; \k<w> is Ruby; \g<w> is Python; $+{w} is Perl. Five spellings for five groups of engines, and every other cell is either err (Java) or the spelling copied through as text. Python's - cells on these rows are the pattern being refused — (?<w>…) is not Python's spelling — not the template; with (?P<w>…) its \g<w> works, as row 12 shows for \g<0>. Perl is n/a on $<w> and ${w} because in Perl those interpolate real variables — $< is the process's real user id, and a key containing it would not be a key.
  • Rows 9–12, the whole match. $&: Perl and JavaScript. $0: Java, Go, Rust. \0: Ruby — and Python and Perl insert a NUL character, because \0 is an octal escape to them. \g<0>: Python alone.
  • Rows 13–14, a literal dollar. $$ gives one $ in JavaScript, Go and Rust, stays $$ in Python and Ruby (where $ is not special at all), and is a runtime error in Java. \$ gives $ in Perl and Java and stays as two characters in the other five.

The trap

Not that the spellings differ — that they fail silently. Only Java says anything when it gets the wrong template language; the rest either substitute nothing or paste the template in as text, and a replacement that produces $1 in its output is a bug that reaches users. Second, the octal cells: \0 and \1 in Perl and \0 in Python are not "unsupported", they are control characters, and a file written with one in it is corrupt in a way no diff shows.

The practical rule: in any engine that offers a function as the replacement — re.sub(p, lambda m: …), s.replace(re, (m, g1) => …), gsub { }, ReplaceAllStringFunc, replace_with — use it for anything beyond $1. The template language is the wrong place for logic.

Where it is measured in depth

See also