◈ Netsurfer / perl.gg / Non-Destructive Substitution with /r

Non-Destructive Substitution with /r

2026-04-18

This page explains the /r modifier for Perl substitution, which returns a modified copy while leaving the original string untouched. Most languages handle this automatically because strings are immutable. Perl takes a different approach by default, since s/// mutates the variable in place and returns the number of substitutions instead of the new string.
my $original = "Hello World"; $original =~ s~World~Perl~; # $original is now "Hello Perl" # the old value is gone forever
The /r modifier flips that behavior. It returns the modified string and leaves the original untouched. Two characters do all the work, and that single flag separates mutation from 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 arrived, building a modified copy required a two-step workaround. The parentheses force a temporary assignment to happen before the substitution operator binds. The code works and produces the correct result, but it reads like a puzzle to anyone who has not memorized the idiom.
(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. One line now carries the full intent, and you no longer have to worry about operator precedence tripping you up.
my $clean = $dirty =~ s~\s+~ ~gr;

Part 2: HOW /r WORKS

Normal s/// returns the number of successful substitutions. With /r, the return value changes completely, since you now receive the modified string instead of a count. The variable bound by =~ is never modified, because the substitution happens on an internal copy that gets returned to you.
my $text = "aaa bbb aaa"; my $count = ($text =~ s~aaa~zzz~g); # $count is 2 # $text is "zzz bbb zzz"
If the pattern does not match, you still get back an unmodified copy of the original. You always get a string back, and you never get a count or an undefined value.
my $text = "aaa bbb aaa"; my $result = $text =~ s~aaa~zzz~gr; # $result is "zzz bbb zzz" # $text is still "aaa bbb aaa"
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 /r and Perl slaps you with a runtime error, since constants and function return values are read-only. The flag gives you a clean way to transform any value you cannot or should not modify.
# 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 or 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, because each s///r returns a string that feeds directly into the next substitution. Five transformations now live in a single expression, and the original $raw stays untouched. Each substitution hands its result to the next step, which behaves exactly like a Unix pipeline.
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, since map already iterates over a list and /r handles the copying for you. Without /r, you would need a temporary variable inside the block, or you would accidentally modify the source array because $_ in map is an alias to the original elements.
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. Each function takes a string and returns a new one, which means no mutation and no side effects. You can even wrap those functions in a small combinator so you compose the pipeline once and reuse it everywhere.
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. Every single example here preserves the original value, so you can log the raw input, display the transformed version, and never clobber your source data.
#!/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. The /e flag evaluates the replacement side as Perl code, and /r makes the whole operation non-destructive. Together they give you computed transformations without mutation.
# 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, which means you need at least that version to use it. Perl 5.14 is ancient at this point, so if your system still ships with something older, you should probably upgrade anyway. If you maintain legacy code that must run on 5.12 or 5.10, you will have to stick with the (my $copy = $orig) =~ s/// pattern.
# 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 distinct operations. The first one tells Perl to change the variable in place, while the second one hands you a modified copy and leaves the original alone. Once you internalize that distinction, every temporary assignment becomes unnecessary, and every map block that carefully avoids aliasing collapses into a simple one-liner.
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

✓ Document: Donehidden-gems / 17.6 KiB▧