Substitution#

s/// is the search-and-replace operator. Its general form is

$target =~ s/pattern/replacement/modifiers;

It finds the first position where pattern matches in target, replaces that match with replacement, and returns the number of substitutions made. With /g, it does that for every non-overlapping match.

my $x = "Time to feed the cat!";
$x =~ s/cat/hacker/;          # $x is now "Time to feed the hacker!"

If the pattern does not match, the target is unchanged and s/// returns a false value.

Operating on $_#

Like m//, s/// defaults to $_ when no =~ is given:

for (@lines) {
    s/\s+$//;       # trim trailing whitespace from each line
}

Counting substitutions#

The return value of s/// in scalar context is the number of replacements:

my $count = ($text =~ s/\btodo\b/done/gi);
print "cleared $count todos\n";

In boolean context, zero means no match.

The replacement string#

The replacement is a double-quoted string, so variables and escape sequences interpolate:

my $suffix = ".bak";
$file =~ s/$/$suffix/;    # append .bak to the name

$x =~ s/\t/    /g;        # each tab becomes four spaces

Inside the replacement, $1, $2, $&, $+ and friends refer to what the current match just captured:

my $y = "'quoted words'";
$y =~ s/^'(.*)'$/$1/;     # strip surrounding single quotes
# $y is now "quoted words"

Named captures work the same way through $+{name}:

"2026-04-23" =~ s/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/$+{d}\/$+{m}\/$+{y}/;
# result: "23/04/2026"

Escaping in the replacement#

The replacement is interpreted as a double-quoted string, so the double-quote rules apply to it, independently of the pattern. Metacharacters like *, +, ?, $ retain their double-quoted meanings — only $ and @ trigger interpolation:

$x =~ s/(\w+)/[$1]/g;     # wrap every word in brackets
$x =~ s/\$/USD/g;         # replace literal '$' with 'USD'

Backslash escape sequences in the replacement follow the double-quote rules — \t, \n, \x{…} all work. The special case-modification escapes \l, \u, \L, \U, \E, \Q apply in the replacement too:

$x =~ s/(\w+)/\u$1/g;     # capitalise the first letter of each word
$x =~ s/(\w+)/\U$1\E/g;   # uppercase whole word

Warning: \1 versus $1 in the replacement#

A long-grandfathered idiom: writing \1, \2, … in the replacement to mean «the first capture», «the second capture»:

$pattern =~ s/(\W)/\\\1/g;     # works, but is a trap

Perl accepts \1 through \9 in the replacement of s/// for historical compatibility with sed. Use $1 through $9 instead. The reason is that the replacement is a double-quoted string, where \1 normally means a control-A character. The substitution operator’s special-case rule kludges that double-quote interpretation to mean «first capture», but the edges are sharp.

Specifically: combining \1 with /e re-introduces the double-quote meaning. Under /e, the replacement is Perl code; in Perl code, "\1" is control-A:

s/(\d+)/ \1 + 1 /eg;   # warning: \1 better written as $1

A second sharp edge: \1 followed by digit characters is ambiguous. \1000 could mean «first capture, then literal 000», or it could mean «octal 0x40 (@) — and you cannot disambiguate by writing \{1}000»:

s/(\d+)/\1000/;     # ambiguous, best avoided
s/(\d+)/${1}000/;   # unambiguous

The rule for new code: always use $1 in the replacement, never \1. The \1 form is grandfathered, not recommended.

/g — replace all#

Without /g, s/// replaces only the first match. With /g, it replaces every non-overlapping match from left to right:

my $x = "I batted 4 for 4";
$x =~ s/4/four/;         # "I batted four for 4"
$x = "I batted 4 for 4";
$x =~ s/4/four/g;        # "I batted four for four"

/g in substitution is unrelated to /g in matching. Here it just means «do this again and again until the pattern stops matching.»

Zero-length matches and s///g#

A pattern that can match the empty string presents a problem under /g: every position would match again forever. Perl breaks this loop with a specific rule: after a zero-length match, the next attempt at the same position is forbidden to also be zero-length. The engine takes the second-best match instead.

The cleanest demonstration:

$_ = 'bar';
s/\w??/<$&>/g;
# Result: <><b><><a><><r><>

The non-greedy \w?? prefers the empty match. After producing one, the next iteration is forbidden from being zero-length too, so the engine takes the second-best match — a single \w character. Then another empty match, then another character, and so on, alternating until the string is consumed.

The pattern: zero-length matches alternate with one-character matches, and the substitution does not loop forever.

A useful corollary: s/(\d{3})/$1,/g does not insert a comma after every set of three digits correctly — it inserts after the first three from the left, then the next three, regardless of where you wanted the commas. To insert thousands separators, match from the right or use 1 while:

my $n = "1234567";
1 while $n =~ s/^(\d+)(\d{3})/$1,$2/;
# n is now "1,234,567"

The 1 while repeats until the substitution returns false (no more matches), which is exactly what we want for «every position, after settling».

/e — evaluate the replacement#

Under /e, the replacement is Perl code, not a string. The code runs for every match and its return value replaces the match.

my $x = "numbers: 1 2 3 4";
$x =~ s/(\d+)/$1 * 2/ge;      # "numbers: 2 4 6 8"

The match variables $1, $2, … are visible inside the code block.

Practical uses:

# Uppercase the first letter of every sentence.
$text =~ s/(^|\.\s+)(\w)/$1\U$2/g;

# Hex-encode non-ASCII bytes.
$bytes =~ s/([\x80-\xff])/sprintf "\\x%02x", ord $1/ge;

# Apply a lookup table.
my %subst = (red => 0xff0000, green => 0x00ff00, blue => 0x0000ff);
$css =~ s/\b(red|green|blue)\b/sprintf "#%06x", $subst{$1}/ge;

/ee — double-eval#

/ee evaluates twice — first as code, then the result is itself evaluated as Perl. The use case is dynamic variable lookup by name:

our $greeting = "hello";
my $s = "greeting";
$s =~ s/(\w+)/"\$$1"/ee;         # first: '$greeting'; second: 'hello'
# $s is now "hello"

The replacement is a Perl expression — the explicit double-quotes make it produce the string "$greeting". The first e evaluates that expression to the string $greeting. The second e treats that string as Perl, looking up the variable $greeting and yielding hello. The match position is replaced with the value hello.

The use case is narrow and the security implications are substantial: any input that reaches an /ee substitution is effectively executed as Perl. Treat /ee like eval — never on user-supplied data.

/r — non-destructive substitution#

/r returns the new string instead of modifying the target. The target is untouched:

my $orig = "I like dogs";
my $new = $orig =~ s/dogs/cats/r;
# $orig is still "I like dogs"
# $new is "I like cats"

If the pattern does not match, /r returns the original string unchanged:

my $x = "I like dogs";
my $y = $x =~ s/elephants/cougars/r;
# $y eq "I like dogs"

The big win is chaining:

my $slug = $title
    =~ s/[^\w\s-]//gr         # drop punctuation
    =~ s/\s+/-/gr             # collapse spaces to dash
    =~ s/--+/-/gr;            # collapse runs of dashes

Each arrow returns a new string, which the next s///r receives.

\K in substitution#

\K (covered in detail in the anchors and assertions chapter) shines in substitution. The pattern matches a prefix that establishes context, then \K excludes the prefix from the replacement target:

# Without \K — the prefix has to be re-output:
$_ =~ s/(foo)bar/$1/g;

# With \K — the prefix is matched but not part of $&:
$_ =~ s/foo\Kbar//g;

Both replace foobar with foo. The \K form is cleaner: no capture is needed, and the substitution pattern is shorter and faster.

\K works similarly in any substitution where a fixed prefix should match but not be replaced. It is the substitution-side twin of lookbehind.

Substitution and pos()#

Successful s///g advances pos($string) past each match. A zero-length match advances pos by one to break the loop. After substitution, pos is reset.

A subtle interaction: s///g will not re-scan a region that has just been matched. After each successful replacement the match position advances past the matched text — the length of the replacement does not matter. So:

my $x = "aaa";
$x =~ s/a/AA/g;
# $x is "AAAAAA" — three a's, each replaced with AA
# It is NOT an infinite loop: pos() moves past each matched 'a',
# so the just-inserted 'A's are never re-scanned.

This is sometimes surprising for substitutions whose replacement contains characters that the pattern would also match. The rule is «advance past the matched text, never re-scan it».

Alternate delimiters#

Both halves of s/// can use matched brackets; the delimiters do not have to be the same:

s{pattern}{replacement}g;
s<pattern><replacement>g;
s[pattern][replacement]g;
s(pattern)[replacement]g;    # legal, but avoid mixing for clarity

Any printable punctuation works in a single-character form:

s!/usr/local/!/opt/!g;
s#pattern#replacement#;

Useful when the pattern or replacement contains /.

Single-quoted substitution#

s'pattern'replacement' treats both halves as single-quoted — no variable interpolation, no backslash escapes apart from \' and \\:

s'@users'@admins'g;     # literal '@users' becomes literal '@admins'

Rarely needed; mention it for completeness.

Combining with /m, /s, /x#

Substitution modifiers compose with match modifiers, so you can get the full toolbox:

# Trim each line.
$x =~ s/^\s+|\s+$//mg;

# Collapse blank lines.
$x =~ s/\n{2,}/\n\n/g;

# Replace C-style comments across newlines.
$src =~ s{/\*.*?\*/}{}gs;   # /s lets . cross newlines

With /x the pattern is readable, with /s it spans newlines, with /g it repeats.

A real example#

Turn a block of English into a slug — lowercase, hyphens instead of spaces, drop non-letters, collapse and trim:

sub slug {
    my ($s) = @_;
    return lc($s)
        =~ s/[^\w\s-]//gr     # remove punctuation
        =~ s/\s+/-/gr          # spaces to hyphens
        =~ s/-+/-/gr           # collapse hyphens
        =~ s/^-|-$//gr;        # trim leading/trailing hyphens
}

print slug("Hello, World! -- Regex Tutorial");
# prints: hello-world-regex-tutorial

Friedl’s framing — «anything that isn’t required will always be considered successful»#

A pattern’s optional pieces will always succeed if they can match nothing. This applies inside substitution as forcefully as in matching:

my $x = "no horizontal rule here";
$x =~ s/-*/<HR>/;
# Result: "<HR>no horizontal rule here"

The pattern -* matches zero or more dashes — and zero is always available, at the start of the string. <HR> is inserted at position 0. The pattern has succeeded by the engine’s definition; it has failed by yours.

The fix is to require at least one dash:

$x =~ s/-+/<HR>/;       # no match; $x unchanged

The slogan: always consider what will happen if there is no match. Optional pieces are not free; they always succeed by matching nothing somewhere.

Things that look like substitution but aren’t#

tr/// (also written y///) does character-by-character transliteration, not regexp substitution. It accepts two lists of characters; each occurrence of the nth character in the first list becomes the nth character in the second list. No regexp features apply:

my $x = "Hello";
$x =~ tr/A-Za-z/a-zA-Z/;     # swap case
(my $hex = $bytes) =~ tr/\x00-\xff//d;   # delete all bytes — pointless

tr/// is faster than s/// for character-level work because it is not a regexp engine. See tr for full semantics.

See also#

  • s — substitution operator reference.

  • tr — character transliteration; not substitution but often confused for it.

  • quotemeta — escape a string for safe interpolation into a pattern.

  • The modifiers chapter — /g, /e, /r, /ee, and how they interact.

  • The anchors and assertions chapter — \K for «match this prefix but don’t replace it».

  • The performance chapter — s///g zero-length match termination in detail.