Perl Part I · Module 11 · Regular Expressions
tr/// : Transliteration
This operator looks like a simpler substitution and is a different tool. It maps characters one for one — give it two lists and every character in the first becomes the character at the same position in the second — and it takes no pattern at all. A dot in a tr/// list is a dot. That single fact is what the chapter is for, because the resemblance to s/// is close enough that people reach for one and get the other, and the failure is silent: both operators run, both edit the string, and only one of them did what was meant. The curriculum rates this the least important chapter in the module and the rating is fair, since anything tr/// does could be written as a substitution. It earns its place on two grounds: a character-for-character swap says what it means more plainly than a pattern would, and the counting form in section three is genuinely the shortest correct way to ask how many of a given character a string contains.
Intermediate12 min readPerlPart IRegular Expressions
Part I · Module 11 · Chapter 11.15 · tr/// : Transliteration
1. Two Lists, Mapped Position by Position
use strict;
use warnings;
my $s = "u_alu/A";
(my $flat = $s) =~ tr{/}{_};
printf "original : %s\n", $s;
printf "tr{/}{_} : %s\n", $flat;
# A LIST of characters, mapped positionally
(my $rot = "abc") =~ tr/abc/xyz/;
printf "tr/abc/xyz/ on 'abc' : %s\n", $rot;original : u_alu/A
tr{/}{_} : u_alu_A
tr/abc/xyz/ on 'abc' : xyzThe first call turned the slash in a hierarchical pin name into an underscore — the standard way to make a name safe for use as a filename. The second shows the positional mapping: a becomes x, b becomes y, c becomes z, all in one pass.
That is the whole operator. Two lists, matched up by position, applied character by character. It edits in place and returns a count, exactly like s///, and the (my $copy = $orig) =~ ... idiom from Chapter 11.13 works here too.
2. It Is Not a Pattern
Here is the fact that makes this a chapter rather than a footnote.
use strict;
use warnings;
# tr/// takes CHARACTER LISTS, not patterns. Metacharacters are literal.
my $s = "a.b+c";
(my $a = $s) =~ tr/./X/;
printf "tr/./X/ on 'a.b+c' : %s <- the dot is a literal dot\n", $a;
(my $b = $s) =~ s/./X/;
printf "s/./X/ on 'a.b+c' : %s <- the dot is ANY character\n", $b;
# and \d is not a digit class either
(my $c = "a1d2") =~ tr/\d/Z/;
printf "tr/\\d/Z/ on 'a1d2' : %s <- 'd' is the LETTER d\n", $c;tr/./X/ on 'a.b+c' : aXb+c <- the dot is a literal dot
s/./X/ on 'a.b+c' : X.b+c <- the dot is ANY character
tr/\d/Z/ on 'a1d2' : a1Z2 <- 'd' is the LETTER dThe two operators given the same two characters did different things. tr/./X/ replaced the dot, producing aXb+c. s/./X/ replaced the first character, producing X.b+c.
Neither errored. Neither warned. Both edited the string. If you wanted to escape a dot in a filename and typed s where you meant tr, you would have replaced the first letter of the name instead and the script would carry on.
The third line goes further: tr/\d/Z/ does not mean digits. The list is the two characters \ and d, so d was replaced and the digits were left alone — a1d2 became a1Z2. Perl does say something about this, but on stderr at compile time, so it is not in the output block above:
Unrecognized escape \d passed through
Treat that warning as an error. It means you wrote a pattern escape in a place that has no patterns.
Ranges do work, as section three uses, and they are the one piece of notation tr/// shares with character classes: a-z means the letters from a to z.
3. An Empty Replacement List Counts
This is the idiom worth taking away from the chapter.
use strict;
use warnings;
my $row = "u_core/u_alu/A";
# An empty replacement list changes nothing and returns a COUNT
my $slashes = ($row =~ tr{/}{});
printf "slashes in %s : %d\n", $row, $slashes;
printf "unchanged : %s\n", $row;
my $line = "setup 0.42 ns";
printf "lowercase letters : %d\n", ($line =~ tr/a-z//);
printf "digits : %d\n", ($line =~ tr/0-9//);
printf "spaces : %d\n", ($line =~ tr/ //);slashes in u_core/u_alu/A : 2
unchanged : u_core/u_alu/A
lowercase letters : 7
digits : 3
spaces : 2Two slashes, and the string came back unchanged — the second line of output confirms it. With nothing to map to, tr/// counts rather than edits.
The three counts below it are the everyday uses. Seven lowercase letters in setup 0.42 ns (the five of setup plus the two of ns), three digits, two spaces. Each is one short expression with no loop and no split.
This is how you ask structural questions about a line cheaply. How many slashes deep is this instance path? How many commas are in this list? How many tabs are in this row — which is precisely the diagnostic Chapter 11.21 needs, because a pattern written for spaces fails silently on tab-aligned output and tr/\t// is the one-line test for it. Chapter 11.16 uses tr/\n// the same way to count the lines in a slurped file.
4. Deleting and Squeezing
Two modifiers extend the operator past one-for-one mapping.
use strict;
use warnings;
my $messy = "u_alu // A";
# /d deletes the matched characters
(my $d = $messy) =~ tr{/}{}d;
printf "/d (delete slashes) : [%s]\n", $d;
# /s squeezes runs of the SAME replacement character into one
(my $sq = "a b c") =~ tr/ / /s;
printf "/s (squeeze spaces) : [%s]\n", $sq;
# combined: delete slashes then squeeze spaces
(my $both = $messy) =~ tr{/}{}d;
$both =~ tr/ / /s;
printf "both : [%s]\n", $both;/d (delete slashes) : [u_alu A]
/s (squeeze spaces) : [a b c]
both : [u_alu A]/d deletes every character in the search list. Note that without it, an empty replacement list counts instead of deleting — so tr{/}{} and tr{/}{}d are two quite different operations separated by one letter.
/s squeezes a run of identical output characters down to one. Mapping space to space with /s collapses runs of spaces, which is the classic whitespace normaliser.
The third line applies both in sequence to get from u_alu // A to u_alu A. That pair — delete the junk, squeeze the spaces — is most of what "clean up this tool output before parsing it" means in practice.
5. Unequal Lists, and /r
use strict;
use warnings;
# If the replacement list is SHORTER, its last character is reused.
(my $a = "abcde") =~ tr/abcde/xy/;
printf "tr/abcde/xy/ : %s <- y reused for c, d, e\n", $a;
# /r works on tr too, returning a copy
my $orig = "u_alu/A";
my $copy = $orig =~ tr{/}{_}r;
printf "original : %s\n", $orig;
printf "copy /r : %s\n", $copy;tr/abcde/xy/ : xyyyy <- y reused for c, d, e
original : u_alu/A
copy /r : u_alu_AFive search characters, two replacements, and the result is xyyyy: a became x, and b, c, d and e all became y because the last replacement character is reused for the remainder.
That is occasionally what you want — tr/0-9/#/ masks every digit with one character — and otherwise it is a silent bug, because mismatched list lengths do not warn. Count your lists, or use a range on both sides so the lengths are obviously equal.
/r behaves as it does for s///: it leaves the original alone and returns the changed copy, which makes tr/// usable inside map.
6. A Worked Case: Normalising a Row Before Parsing
This is where the counting and the two modifiers earn their keep together — cleaning a row so a pattern can be written against a predictable shape.
use strict;
use warnings;
# A row as a tool emitted it: tabs, doubled spaces, trailing whitespace.
my $raw = "u_alu/A\t\t-0.12 ns \n";
printf "tabs before : %d\n", ($raw =~ tr/\t//);
printf "newlines : %d\n", ($raw =~ tr/\n//);
# tr/// for the character-level work, in measured steps
(my $row = $raw) =~ tr/\n//d; # drop the newline
my $tabs = ($row =~ tr/\t/ /); # tabs -> spaces, counted
$row =~ tr/ / /s; # squeeze runs of spaces
printf "tabs replaced : %d\n", $tabs;
printf "after tr/// : [%s] <- note the trailing space\n", $row;
# tr/// cannot anchor, so trailing whitespace needs s///
$row =~ s/\s+$//;
printf "after s/// : [%s]\n", $row;
printf "fields now : %d\n", scalar(my @f = split ' ', $row);tabs before : 2
newlines : 1
tabs replaced : 2
after tr/// : [u_alu/A -0.12 ns ] <- note the trailing space
after s/// : [u_alu/A -0.12 ns]
fields now : 3Two tabs and one newline on the way in; three clean fields on the way out.
Every step is measured rather than assumed. The counts before the edits confirm the row really did contain tabs — which, per Chapter 11.21, is the diagnosis you cannot make by looking at it. tr/\t/ / returns how many it converted, and tr/ / /s collapses the runs.
Then the honest limitation: tr/// cannot anchor. It has no notion of the start or end of a string, so the trailing space survives every transliteration you can write, and removing it needs s/\s+$//. That is the clean division of labour between the two operators — tr/// for "these characters, anywhere", s/// for anything that depends on position.
7. What To Remember
tr///maps characters one for one, by position, in a single pass. It edits in place and returns a count.- It takes character lists, not a pattern. Measured:
tr/./X/changed the literal dot wheres/./X/changed the first character — neither warned. \din atr///list is the two characters\andd, and Perl saysUnrecognized escape \d passed throughon stderr at compile time. Treat it as an error.- Having no pattern language is a safety property. There is nothing to escape, so there is no
\Qto forget. - An empty replacement list counts and leaves the string unchanged —
$n = ($s =~ tr/\t//)is the shortest correct way to ask how many tabs a line has. /ddeletes the search characters.tr{/}{}counts;tr{/}{}ddeletes./ssqueezes runs of the same output character into one, which is the whitespace normaliser.- A shorter replacement list reuses its last character, silently.
/rreturns a copy here too.- It is rated Medium honestly.
s///can do all of it;tr///wins on clarity for fixed character mappings and on the counting idiom.
Chapter 11.16 moves to the two modifiers that matter once a string holds more than one line. Read a whole report into one scalar and the anchors stop meaning what they meant: /m makes ^ and $ match at every line, and /s lets the dot cross a newline.
Where this fits
Part of the Perl curriculum.
