$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,
$1versus\1. The split, exactly as advertised:$2$1givesbain five engines and is left as text in Python and Ruby;\2\1givesbain Python and Ruby, becomes two control characters in Perl (octal escapes in a string), the digits21in Java (backslash quotes the next character), and stays as the four characters\2\1in 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 eithererr(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 isn/aon$<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\0is 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¶
- Backreferences in the replacement — why
$1on the right is not a backreference, and whatseddoes.
See also¶
- Capturing group · Named group · Backreference — the pattern side.
$— the other dollar.- Search and replace.