Skip to content

Backreferences in the replacement

One line: The \1 on the right-hand side of a substitution is not a backreference — the matching is already over, and it is an instruction to the text builder.

Level: 101 · anyone who has written s/…/…/ or re.sub

Keywords on this page: $1 in the replacement · (…) · \1 — Topics: Search and replace · Escaping and literal text

They look identical and they are different mechanisms. In a pattern, \1 constrains what can match. In a replacement, \1 is a placeholder filled in after the match succeeded. The confusion is not the reader's fault: sed uses \1 for both, and most of us met sed first.

The second half of the confusion is that the languages disagree about the character. Roughly: Perl, JavaScript and ABAP say $1; Python and sed say \1.

Python — \1, and \g<…> when it gets awkward

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

DE44 5001-0517 5407-3249 31
12340
09/2026
order [42]
order $1

Three things to take from that:

  • \g<1> is the unambiguous form. re.sub(r"(\d{4})", r"\10", "1234") is an error — Python reads \10 as group 10 — and \g<1>0 says what you meant.
  • \g<0> is the whole match, the equivalent of sed's & and Perl's $&.
  • The last line prints order $1. In Python, $1 in a replacement is three ordinary characters. Nothing warns you; the output is simply wrong in a way that survives review.

Note the first line too: re.sub replaces every occurrence by default, which is why two groups of digits get hyphenated there and only one does in the Perl and JavaScript examples below. s/// and .replace() change the first match unless you ask for more.

Perl — the groups are just variables

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

DE44 5001-0517 5407 3249 31
order [42]
12340
order <42>

Because the right-hand side of s/// is Perl code, $1 is the same $1 you would use anywhere else. ${1} is the form that survives a following digit, and /r returns a modified copy instead of editing in place — the habit worth having when the original is someone else's data.

JavaScript — $1, plus the escape hatch

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

DE44 5001-0517 5407 3249 31
09/2026
order [42]
$cost
order 84

$<name> is the named form, $& is the whole match, and $$ is how you write one literal dollar. The last line is the one that matters most in practice: when the replacement needs computation — arithmetic, a lookup, a conditional — every engine on this page eventually makes you leave the pattern language, and JavaScript's function replacement is the cleanest version of that exit.

sed — where \1 means both things in one line

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

DE44 5001-0517
order [42]
DOUBLED: the the cat

Read that last command carefully: s/\([a-z][a-z]*\) \1/DOUBLED: &/p. The \1 on the left is a genuine backreference constraining the match; the & on the right is the whole matched text. Both spellings live in one expression, which is exactly why the two ideas blur together.

(\w is a GNU extension. BSD sed — the one on a Mac — does not have it, so the portable spelling is a bracket expression.)

ABAP — $1, and the addition that turns it off

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

REPORT z_replacement.

" In ABAP the replacement pattern uses $1, like JavaScript and Perl -- not \1.
" Source only: nothing here can run ABAP. Checked by abaplint.

DATA(iban) = `DE44 5001 0517 5407 3249 31`.

REPLACE ALL OCCURRENCES OF PCRE `(\d{4}) (\d{4})`
        IN iban WITH `$1-$2`.

" $0 is the whole match, and ${1} is the form that survives a following digit.
DATA(order) = `order 42`.
REPLACE ALL OCCURRENCES OF PCRE `\d+` IN order WITH `[$0]`.

" When the replacement text is DATA rather than a pattern, the addition
" VERBATIM turns off the special meaning of $ instead of hoping the data has
" none. It is shown on the lesson page rather than here: abaplint 2.120.19
" does not parse it yet, and this file is kept lint-clean.

ABAP follows Perl here: $1 for a group, $0 for the whole match, ${1} when a digit follows.

The interesting addition is VERBATIM:

" Sketch — not in the example file above, and not run: abaplint 2.120.19 does
" not parse VERBATIM yet, so keeping it in the file would fail the lint.
REPLACE ALL OCCURRENCES OF PCRE `total` IN price WITH `$5` VERBATIM.

The interesting addition is VERBATIM. When the replacement text is data rather than a pattern — a value from a table, something a user typed — any $ in it is a live control character, and a customer whose name contains $1 will produce a bug that only appears for that customer. VERBATIM says "take these characters literally". Reach for it whenever the replacement is not a literal you wrote yourself. (JavaScript's equivalent is escaping each $ as $$; Python's is re.escape-style care, or a function replacement.)

The cross-language table

Group n Whole match Literal $ / digit-safe Compute the replacement
Python \1, \g<1>, \g<name> \g<0> \g<1>0 pass a function to re.sub
Perl $1, ${1} $& ${1}0 /e flag
JavaScript $1, $<name> $& $$, ${1} not supported pass a function to .replace
sed \1 & \\& to escape —
ABAP (PCRE) $1, ${1} $0 VERBATIM cl_abap_matcher + code
Rust regex $1, ${1} $0 $$ replace_all with a closure

Try it

  1. Take a string containing a literal $1 and substitute something into it with your language's default replacement. Then fix it. (Python: nothing to fix. JavaScript: $$. ABAP: VERBATIM.)
  2. Swap two captured groups — (\w+), (\w+) → $2 $1. Then add a group at the front of the pattern and watch every number shift. Now do it with names.
  3. In sed, write a substitution whose pattern and replacement both use group 1, and say out loud which one is the backreference.

See also