Skip to content
VLSI Mentor

Perl Part I · Module 11 · Regular Expressions

s/// : Substitution

Everything in this module so far has read text. The substitution operator writes it, and it is the reason so much report-rewriting and log-normalising work in this field is done in Perl at all: find a pattern, put something else there, and the whole edit is one expression. Two things about it surprise people, and both are measured here. It edits the variable you bind it to rather than returning a new string, so assigning its result gets you a number instead of your text — and that number is a count of replacements, which is genuinely useful once you know to expect it. And because it has to write back, its left-hand side must be something assignable, which is a restriction the matching operator does not have. Section five shows what that looks like when it fails: not a compile error but a run-time death, at the moment the operator runs.

Intermediate11 min readPerlPart IRegular Expressions

Part I · Module 11 · Chapter 11.13 · s/// : Substitution

1. Find, and Put Something Else There

The operator takes two parts: a pattern, and a replacement.

Azvya Education Pvt. Ltd.VLSI Mentor
basic.pl
use strict;
use warnings;

my $line = "slack -0.12 ns";

my $n = ($line =~ s/ns/nanoseconds/);

printf "returned : %s\n", $n;
printf "variable : %s\n", $line;
print  "\ns/// edits the variable IN PLACE and returns a COUNT\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
returned : 1
variable : slack -0.12 nanoseconds

s/// edits the variable IN PLACE and returns a COUNT

Two facts in three lines of output. $line changed — the operator edited the variable it was bound to. And the expression returned 1, not the new text.

That second fact is the one that costs people time. my $new = ($old =~ s/a/b/); looks like it should put the edited string in $new, and instead puts the number 1 there while quietly editing $old. If you want a changed copy and an untouched original, that is the /r modifier, which is Chapter 11.14's first order of business.

2. The Return Value Is a Count

Because the return is a count, you get the edit and a measurement in one statement.

Azvya Education Pvt. Ltd.VLSI Mentor
count.pl
use strict;
use warnings;

for my $subject ("a-b-c", "abc", "a-b-c-d-e") {
    my $copy = $subject;
    my $n = ($copy =~ s/-/_/g);
    printf "%-10s -> %-10s  replacements: %d\n", $subject, $copy, $n;
}
print "\nzero replacements returns the EMPTY STRING, not 0\n";
my $z = "abc";
my $r = ($z =~ s/-/_/g);
printf "no-match return : [%s]  defined:%s  true:%s\n",
    $r, (defined $r ? "y" : "n"), ($r ? "y" : "n");
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
a-b-c      -> a_b_c       replacements: 2
abc        -> abc         replacements: 0
a-b-c-d-e  -> a_b_c_d_e   replacements: 4

zero replacements returns the EMPTY STRING, not 0
no-match return : []  defined:y  true:n

Two, zero and four replacements. (The /g there is Chapter 11.14's modifier, borrowed so the counts can exceed one — without it every row would say 1 or 0.)

Now read the last line against the row above it. The table printed 0 for abc, but that is printf "%d" numifying the value. The actual return on no match is the empty string: defined, and false.

This is the same value m// returns on failure, measured in Chapter 11.2, and it has the same consequence. defined $n is always true and tests nothing. $n == 0 works, because "" numifies to zero. The cleanest test is the boolean one — if ($n) { ... } for "something changed", or unless ($n) for "nothing matched".

3. Captures in the Replacement

The replacement half is interpolated like a double-quoted string, so the capture variables work in it.

Azvya Education Pvt. Ltd.VLSI Mentor
captures-in-replacement.pl
use strict;
use warnings;

my $line = "u_alu/A 0.42";

my $copy = $line;
$copy =~ s{^(\S+)\s+(\S+)$}{$2 from $1};
printf "reordered : %s\n", $copy;

# $& is the whole match
my $c2 = $line;
$c2 =~ s{\d+\.\d+}{[$&]};
printf "wrapped   : %s\n", $c2;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
reordered : 0.42 from u_alu/A
wrapped   : u_alu/A [0.42]

The first substitution swapped two fields by matching them into $1 and $2 and writing them back in the other order. The second wrapped whatever matched in brackets using $&, the whole match from Chapter 11.10.

Because the replacement is an interpolated string, everything you know about double quotes applies — including the hazards from Chapter 11.4. A literal $ in the replacement needs escaping, and a literal @ will try to interpolate an array. The s{...}{...} form used here is worth adopting for anything non-trivial: the two halves are visually separate, and a slash in either one needs no escaping.

4. It Must Be Able To Write Back

A match can read any expression. A substitution has to store its result, so its target must be assignable.

Azvya Education Pvt. Ltd.VLSI Mentor
needs-lvalue.pl
use strict;
use warnings;

# s/// must write back, so its target must be assignable.
my %h = (unit => "ns");
my @a = ("ns");

$h{unit} =~ s/ns/ps/;
$a[0]    =~ s/ns/ps/;

printf "hash element : %s\n", $h{unit};
printf "array element: %s\n", $a[0];

# A read-only value DIES when the operator runs.
my $err = "";
eval { my $x = "ns"; for my $lit ("ns") { $lit =~ s/ns/ps/ } 1 } or $err = $@;
printf "on a literal-backed alias : %s\n", ($err ? "died" : "ok");
printf "  message: %s", $err if $err;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
hash element : ps
array element: ps
on a literal-backed alias : died
  message: Modification of a read-only value attempted at needs-lvalue.pl line 16.

Hash elements and array elements are assignable, so both were edited in place — which is what makes s/// usable directly on a parsed record without copying fields out first.

The third case is the failure. for my $lit ("ns") aliases the loop variable to a literal, and a literal cannot be modified, so the substitution dies at run time with Modification of a read-only value attempted.

The detail worth keeping is when it dies. This is not a compile error — the program ran, printed two lines, and then died on reaching the operator. Chapter 9.2 measured the same thing from the loop side and found something sharper still: the death happens whenever the operator runs, even if the pattern could never have matched. There is no "it did not match, so nothing was written" escape. If s/// executes against a read-only value, the program stops.

So a foreach over a literal list is read-only; a foreach over an array of variables is not, because Chapter 9.2's aliasing makes the loop variable the real element.

5. Delimiters

As with m//, the slashes are a default rather than a requirement.

Azvya Education Pvt. Ltd.VLSI Mentor
delimiters.pl
use strict;
use warnings;

my $p = "/proj/lib/and2.lib";

my $a = $p; $a =~ s/\/proj/\/scratch/;
my $b = $p; $b =~ s{/proj}{/scratch};
my $c = $p; $c =~ s!/proj!/scratch!;

printf "s///  : %s\n", $a;
printf "s{}{} : %s\n", $b;
printf "s!!!  : %s\n", $c;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
s///  : /scratch/lib/and2.lib
s{}{} : /scratch/lib/and2.lib
s!!!  : /scratch/lib/and2.lib

All three produce the same path. The first is the version that makes path rewriting painful to read: four backslashes that carry no meaning beyond protecting the delimiter.

With a paired delimiter the two halves get their own brackets, as in s{...}{...}, which is why that form is the house style here. With a non-paired delimiter the three parts run together — s!a!b! — which is fine for something short.

6. Editing Every Element of an Array

Because foreach aliases the element rather than copying it, a substitution inside the loop edits the array itself — and the counts add up to a verification.

Azvya Education Pvt. Ltd.VLSI Mentor
edit-in-place.pl
use strict;
use warnings;

# foreach aliases the element, so s/// edits the ARRAY itself
my @units = ("1 ns", "2 ns", "3 ns");
my $edits = 0;
for my $u (@units) {
    $edits += ($u =~ s/ns/ps/);
}
printf "array after : %s\n", join(", ", @units);
printf "edits made  : %d\n", $edits;

# The count is a verification: did EVERY element change?
printf "all changed : %s\n", ($edits == @units ? "yes" : "NO");

# A row that does not contain the pattern contributes nothing
my @mixed = ("1 ns", "2 ps", "3 ns");
my $n = 0;
$n += ($_ =~ s/ns/ps/) for @mixed;
printf "\nmixed after : %s\n", join(", ", @mixed);
printf "edits made  : %d of %d element(s)\n", $n, scalar @mixed;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
array after : 1 ps, 2 ps, 3 ps
edits made  : 3
all changed : yes

mixed after : 1 ps, 2 ps, 3 ps
edits made  : 2 of 3 element(s)

The array was modified in place with no assignment back to it, because $u is the element — Chapter 9.2's aliasing.

The accumulated count is the part worth adopting. $edits == @units asks "did every element change?", and on the second array the answer is 2 of 3 — correctly, since one element was already in picoseconds. That is a cheap integrity check on a bulk edit: if you expected every row to be rewritten and the count comes back short, the shortfall is the number of rows that did not look the way you assumed.

7. What To Remember

  • s/// edits the variable in place and returns a count, not the new string. my $new = ($old =~ s/a/b/) puts 1 in $new and edits $old.
  • On no match the return is the empty string, defined and false — the same value m// fails with. Test it as a boolean; defined tells you nothing.
  • Keep the count. It turns a blind edit into a measurement: how many tabs, how many fields rewritten.
  • The replacement is an interpolated string, so $1, $2 and $& work in it — and a literal $ or @ needs escaping.
  • The target must be assignable. Hash and array elements are fine.
  • A read-only target dies at run time, not at compile time, with Modification of a read-only value attempted — and per Chapter 9.2 it dies whenever the operator runs, even if the pattern cannot match.
  • s{...}{...} is the readable form for anything containing slashes.

Chapter 11.14 adds the four modifiers that do most of the real work: /g to replace every match rather than the first, /i to ignore case, /e to treat the replacement as code to run, and /r to return a changed copy instead of editing the original.

Where this fits

Part of the Perl curriculum.