perl.gg / hidden-gems

<!-- category: hidden-gems -->

Non-Destructive Substitution with /r

2026-04-18

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.

my $original = "Hello World"; $original =~ s~World~Perl~; # $original is now "Hello Perl" # the old value is gone forever
The /r modifier flips this. It returns the modified string and leaves the original untouched:
my $original = "Hello World"; my $modified = $original =~ s~World~Perl~r; # $modified is "Hello Perl" # $original is still "Hello World"
Two characters. One flag. The difference between mutation and transformation.

Part 1: THE OLD WAY

Before /r, if you wanted a modified copy you had to do this awkward two-step:
(my $clean = $dirty) =~ s~\s+~ ~g;
Read that carefully. It declares $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:

my $clean = $dirty =~ s~\s+~ ~g; # WRONG: $clean gets the substitution count (a number) # and $dirty is mutated (which you didn't want)
The /r flag makes this whole dance unnecessary:
my $clean = $dirty =~ s~\s+~ ~gr;
One line. Clear intent. No gotchas about operator precedence.

Part 2: HOW /r WORKS

Normal s~~~ returns the number of successful substitutions:
my $text = "aaa bbb aaa"; my $count = ($text =~ s~aaa~zzz~g); # $count is 2 # $text is "zzz bbb zzz"
With /r, the return value changes. Instead of a count, you get the modified string:
my $text = "aaa bbb aaa"; my $result = $text =~ s~aaa~zzz~gr; # $result is "zzz bbb zzz" # $text is still "aaa bbb aaa"
The variable bound by =~ 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:

my $text = "Hello"; my $result = $text =~ s~xyz~abc~r; # $result is "Hello" (a copy, not the same reference) # $text is "Hello"
You always get a string back. Never a count. Never undef.

Part 3: USING WITH IMMUTABLE VALUES

The /r flag lets you substitute against things you could never modify with regular s~~~:
# substitute against a literal string my $result = "2026-04-18" =~ s~-~/~gr; # $result is "2026/04/18"
Try that without /r and Perl slaps you:
Can't modify constant item in substitution (s///) at ...
Same with function return values:
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;
And hash/array values in read-only contexts:
my %config = (path => "/usr/local/bin"); # make a modified copy without touching the hash my $escaped = $config{path} =~ s~/~\\/~gr;
Anywhere you have a value you cannot or should not modify, /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:
my $clean = $raw =~ s~\r\n~\n~gr =~ s~<[^>]*>~~gr =~ s~&amp;~&~gr =~ s~\s{2,}~ ~gr =~ s~^\s+|\s+$~~gr;
Five transformations. One expression. The original $raw is untouched. Each substitution feeds its result to the next, like a Unix pipeline.

Without /r, this would be:

my $clean = $raw; $clean =~ s~\r\n~\n~g; $clean =~ s~<[^>]*>~~g; $clean =~ s~&amp;~&~g; $clean =~ s~\s{2,}~ ~g; $clean =~ s~^\s+|\s+$~~g;
Six statements. A mutable variable being stomped on five times. Functional programmers cry. The /r chain is a single expression that can be used anywhere an expression is expected.

Part 5: MAP AND /r

The combination of map and s~~~r is beautiful:
my @names = (" John Doe ", "Jane Smith", " Bob "); my @clean = map { s~^\s+|\s+$~~gr } @names; # @clean is ("John Doe", "Jane Smith", "Bob") # @names is untouched
Without /r, you would need a temporary:
my @clean = map { (my $tmp = $_) =~ s~^\s+|\s+$~~g; $tmp; } @names;
Or worse, you accidentally modify @names because $_ in map is aliased to the original elements:
# DANGER: this modifies @names! my @clean = map { s~^\s+|\s+$~~g; $_ } @names;
That $_ 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:
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); }
Each function takes a string and returns a new one. No mutation. No side effects. Pure transformation.

You could even write a pipeline combinator:

sub 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);
Now $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:
#!/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]
Every single one of these preserves the original value. You can log the original, display the transformed version, and nothing gets clobbered.

Part 8: /r WITH /e

You can combine /r with the /e flag for computed replacements:
# 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)
The /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:

my $title = "the quick brown fox"; my $capped = $title =~ s~\b(\w)~uc($1)~ger; say $capped; # The Quick Brown Fox
Obfuscate an email address for display:
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.
# Check your version say $^V; # v5.40.0 or whatever you're running
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 (my $copy = $orig) =~ s~~~ pattern.

Here is a compatibility wrapper if you truly need one:

sub subst { my ($str, $pattern, $replacement, $flags) = @_; $flags //= ''; my $copy = $str; eval "\$copy =~ s~\$pattern~$replacement~$flags"; return $copy; }
But really, just upgrade your Perl.

Part 10: THE MENTAL MODEL

Think of s~~~ and s~~~r as two different operations:
s~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." (| | ) /'\_ _/`\ \___)=(___/
The /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