Skip to content

s/// and tr/// return counts

Level: 101 · you wrote my $clean = $dirty =~ s/.../.../

One line: s/// and tr/// change their target and return how many substitutions or characters they found — so my $new = $old =~ s/a/b/g stores a number, map { s/^\s+// } @lines returns counts and edits the array, and a failed s/// returns the empty string — unless you add /r, which leaves the target alone and returns the new string.

Measured

Verified output of s_and_tr_pl.pl — regenerated by tools/run_examples.py, never hand-typed.

--- s/// returns a count ---
  returned:  2
  target:    /opt/local/bin:/opt/bin:/bin
  no match:  ''   (the empty string, not 0)

--- so map { s/// } returns counts ---
  map { s/^\s+|\s+$//g }   returned '1', '1', ''
                            and changed the array: 'alpha', 'beta', 'gamma'
  map { s/^\s+|\s+$//gr }  returned 'alpha', 'beta', 'gamma'
                            and left the array: '  alpha', 'beta  ', 'gamma'

--- s///e runs the replacement as code ---
  tea 5.54, cake 14.76

--- tr/// counts characters, and is not a regex ---
  tr/s//          4   and the target is still Mississippi
  tr/a-zA-Z//cdr  HelloWorld
  tr/./_/r        1_2_3   a dot is a dot
  tr/a-z// on 'zażółć' counts 2   the range is code points 0x61 to 0x7A
  tr/$letter// on 'letters' counts 6   the set is $ l e t r, not s
  eval "tr/$letter//" on 'letters' counts 1

s/// edits and counts

$path =~ s{/usr}{/opt}g changed $path and returned 2, the number of replacements. With no match it returned '' — false, but not 0, so printing it shows nothing.

map with s///

map aliases $_ to each element, so s/// inside it edits the array itself, and map collects what s/// returned: 1, 1, ''. Both results are wrong for "give me the trimmed lines" — the list is counts, and the source is gone. /r fixes both: it works on a copy and returns the copy.

s///e

With /e the replacement is Perl code, evaluated per match: sprintf '%.2f', $1 * 1.23 turns each price into a price with 23% added.

tr/// is not a regex

tr — also spelled y — maps characters to characters. It returns the count of characters it matched, so tr/s// with an empty replacement counts without changing anything. And it is not a pattern:

  • . is a dot. tr/./_/r replaces dots, not every character.
  • a-z is a range of code points, 0x61 to 0x7A. ż, ó, ł and ć are outside it, so zażółć counts 2.
  • Variables are not interpolated. tr/$letter// is the set of characters $, l, e, t, r — it counts 6 in letters, and never looks at $letter. perlop documents string eval as the way to build a tr from a variable, which the last line shows; with data you did not write, build a character class for s/// instead.

What to do instead

my $clean   = $dirty =~ s/\s+\z//r;           # a new string
my @trimmed = map { s/^\s+|\s+\z//gr } @lines;
my $count   = ($text =~ tr/\n//);             # count characters
(my $copy = $original) =~ s/a/b/g;            # the older idiom for a copy, before /r

If you are coming from another language

  • Python. str is immutable, so re.sub always returns a new string — Perl's /r is the default there — and re.subn returns the count alongside it. str.translate is tr.
  • The tr command. The encodings library's tr and sort work a byte at a time ↗ — Perl's tr counts code points once the string is decoded; the shell's counts bytes.

See also