<!-- category: hidden-gems -->
Non-Destructive Substitution with /r
You have a string. You want a modified copy. You do not want to touch the original.In most languages, this is the default. Strings are immutable.
You always get a copy. In Perl, s~~~ mutates in place. It
changes the variable and returns the count of substitutions.
Themy $original = "Hello World"; $original =~ s~World~Perl~; # $original is now "Hello Perl" # the old value is gone forever
/r modifier flips this. It returns the modified string
and leaves the original untouched:
Two characters. One flag. The difference between mutation and transformation.my $original = "Hello World"; my $modified = $original =~ s~World~Perl~r; # $modified is "Hello Perl" # $original is still "Hello World"
Part 1: THE OLD WAY
Before/r, if you wanted a modified copy you had to do
this awkward two-step:
Read that carefully. It declares(my $clean = $dirty) =~ s~\s+~ ~g;
$clean, assigns $dirty to
it, then mutates $clean in place. The parentheses force the
assignment to happen before the substitution binds.
It works. It is correct. It is also ugly and confusing to anyone who has not memorized this idiom.
Without the parens, it breaks:
Themy $clean = $dirty =~ s~\s+~ ~g; # WRONG: $clean gets the substitution count (a number) # and $dirty is mutated (which you didn't want)
/r flag makes this whole dance unnecessary:
One line. Clear intent. No gotchas about operator precedence.my $clean = $dirty =~ s~\s+~ ~gr;
Part 2: HOW /r WORKS
Normals~~~ returns the number of successful substitutions:
Withmy $text = "aaa bbb aaa"; my $count = ($text =~ s~aaa~zzz~g); # $count is 2 # $text is "zzz bbb zzz"
/r, the return value changes. Instead of a count, you
get the modified string:
The variable bound bymy $text = "aaa bbb aaa"; my $result = $text =~ s~aaa~zzz~gr; # $result is "zzz bbb zzz" # $text is still "aaa bbb aaa"
=~ is never modified. The substitution
happens on an internal copy, and that copy is what gets returned.
If the pattern does not match, you get back an unmodified copy of the original:
You always get a string back. Never a count. Never undef.my $text = "Hello"; my $result = $text =~ s~xyz~abc~r; # $result is "Hello" (a copy, not the same reference) # $text is "Hello"
Part 3: USING WITH IMMUTABLE VALUES
The/r flag lets you substitute against things you could
never modify with regular s~~~:
Try that without# substitute against a literal string my $result = "2026-04-18" =~ s~-~/~gr; # $result is "2026/04/18"
/r and Perl slaps you:
Same with function return values:Can't modify constant item in substitution (s///) at ...
And hash/array values in read-only contexts:sub get_name { return "John Doe" } # without /r - need a temporary variable my $name = get_name(); $name =~ s~\s+~_~g; # with /r - direct my $name = get_name() =~ s~\s+~_~gr;
Anywhere you have a value you cannot or should not modify,my %config = (path => "/usr/local/bin"); # make a modified copy without touching the hash my $escaped = $config{path} =~ s~/~\\/~gr;
/r
gives you a clean way to transform it.
Part 4: CHAINING SUBSTITUTIONS
This is where/r really shines. Each s~~~r returns a string,
so you can chain them:
Five transformations. One expression. The originalmy $clean = $raw =~ s~\r\n~\n~gr =~ s~<[^>]*>~~gr =~ s~&~&~gr =~ s~\s{2,}~ ~gr =~ s~^\s+|\s+$~~gr;
$raw is
untouched. Each substitution feeds its result to the next, like
a Unix pipeline.
Without /r, this would be:
Six statements. A mutable variable being stomped on five times. Functional programmers cry. Themy $clean = $raw; $clean =~ s~\r\n~\n~g; $clean =~ s~<[^>]*>~~g; $clean =~ s~&~&~g; $clean =~ s~\s{2,}~ ~g; $clean =~ s~^\s+|\s+$~~g;
/r chain is a single expression
that can be used anywhere an expression is expected.
Part 5: MAP AND /r
The combination ofmap and s~~~r is beautiful:
Withoutmy @names = (" John Doe ", "Jane Smith", " Bob "); my @clean = map { s~^\s+|\s+$~~gr } @names; # @clean is ("John Doe", "Jane Smith", "Bob") # @names is untouched
/r, you would need a temporary:
Or worse, you accidentally modifymy @clean = map { (my $tmp = $_) =~ s~^\s+|\s+$~~g; $tmp; } @names;
@names because $_ in
map is aliased to the original elements:
That# DANGER: this modifies @names! my @clean = map { s~^\s+|\s+$~~g; $_ } @names;
$_ inside map is not a copy. It is an alias. Mutating
it mutates the source array. This is one of those bugs you only
make once, and it ruins your afternoon when you do.
The /r version has none of these problems. It creates a copy
automatically.
Part 6: FUNCTIONAL DATA PIPELINES
Build a text processing pipeline using/r and subroutine
references:
Each function takes a string and returns a new one. No mutation. No side effects. Pure transformation.my @transforms = ( sub { $_[0] =~ s~\t~ ~gr }, # tabs to spaces sub { $_[0] =~ s~\r\n?~\n~gr }, # normalize newlines sub { $_[0] =~ s~[^\x20-\x7E\n]~~gr }, # strip non-printable sub { $_[0] =~ s~\s+$~~gmr }, # trim trailing whitespace sub { $_[0] =~ s~\n{3,}~\n\n~gr }, # collapse blank lines ); my $result = $raw_text; for my $fn (@transforms) { $result = $fn->($result); }
You could even write a pipeline combinator:
Nowsub pipeline { my @fns = @_; return sub { my $val = shift; $val = $_->($val) for @fns; return $val; }; } my $sanitize = pipeline( sub { $_[0] =~ s~<script[^>]*>.*?</script>~~gsir }, sub { $_[0] =~ s~<[^>]+>~~gr }, sub { $_[0] =~ s~\s+~ ~gr }, sub { $_[0] =~ s~^\s+|\s+$~~gr }, ); my $safe_text = $sanitize->($user_input);
$sanitize is a reusable function that applies all four
transformations in order. Compose it once, use it everywhere.
Part 7: PRACTICAL TEXT CLEANING
Real-world cleanup tasks where/r keeps things tidy:
Every single one of these preserves the original value. You can log the original, display the transformed version, and nothing gets clobbered.#!/usr/bin/env perl use strict; use warnings; use feature 'say'; # Normalize a phone number my $phone = "(416) 555-1234"; my $digits = $phone =~ s~\D~~gr; say $digits; # 4165551234 # Slugify a title my $title = " My Blog Post: A Story! "; my $slug = lc($title) =~ s~^\s+|\s+$~~gr =~ s~[^a-z0-9\s-]~~gr =~ s~\s+~-~gr; say $slug; # my-blog-post-a-story # CSV field quoting my @fields = ("hello", 'say "hi"', "foo,bar"); my $csv = join ',', map { '"' . ($_ =~ s~"~""~gr) . '"' } @fields; say $csv; # "hello","say ""hi""","foo,bar" # Mask sensitive data my $log = "User admin logged in from 192.168.1.42"; my $safe = $log =~ s~\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}~[REDACTED]~gr; say $safe; # User admin logged in from [REDACTED]
Part 8: /r WITH /e
You can combine/r with the /e flag for computed replacements:
The# Increment all numbers in a string, non-destructively my $text = "v2.14.3"; my $bumped = $text =~ s~(\d+)~$1 + 1~ger; say $bumped; # v3.15.4 say $text; # v2.14.3 (untouched)
/e flag evaluates the replacement as Perl code. The /r
flag makes it non-destructive. Together, you get computed
transformations without mutation.
Uppercase the first letter of every word:
Obfuscate an email address for display:my $title = "the quick brown fox"; my $capped = $title =~ s~\b(\w)~uc($1)~ger; say $capped; # The Quick Brown Fox
my $email = 'user@example.com'; my $masked = $email =~ s~(.).+@~$1***@~r; say $masked; # u***@example.com
Part 9: WHEN /r WAS ADDED
The/r flag arrived in Perl 5.14 (May 2011). If you are
stuck on something older, you cannot use it.
Perl 5.14 is ancient at this point. If your system has something older, you have bigger problems than missing regex flags. But if you maintain legacy code that must run on 5.12 or 5.10, you are stuck with the# Check your version say $^V; # v5.40.0 or whatever you're running
(my $copy = $orig) =~ s~~~ pattern.
Here is a compatibility wrapper if you truly need one:
But really, just upgrade your Perl.sub subst { my ($str, $pattern, $replacement, $flags) = @_; $flags //= ''; my $copy = $str; eval "\$copy =~ s~\$pattern~$replacement~$flags"; return $copy; }
Part 10: THE MENTAL MODEL
Think ofs~~~ and s~~~r as two different operations:
Thes~old~new~ = "mutate this variable" s~old~new~r = "give me a modified copy" Without /r: With /r: $var -----> $var $var -----> $var (unchanged) "old" "new" "old" | v $copy "new" .--. |o_o | "One letter turns |:_/ | mutation into // \ \ transformation." (| | ) /'\_ _/`\ \___)=(___/
/r flag is one of those features that, once you learn it,
you cannot believe you ever lived without. Every (my $copy = $orig)
=~ s~~~ becomes a clean $orig =~ s~~~r. Every map block that
carefully avoids aliasing becomes a simple one-liner. Every chain
of substitutions becomes a readable pipeline.
It is the difference between telling Perl "change this thing" and telling Perl "show me what this thing would look like if you changed it." One is a command. The other is a question.
Ask more questions. Mutate less.
perl.gg