Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
one-for-one.pl
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
original : u_alu/A
tr{/}{_} : u_alu_A
tr/abc/xyz/ on 'abc' : xyz

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
not-a-pattern.pl
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 d

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
counting.pl
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/ //);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
slashes in u_core/u_alu/A : 2
unchanged           : u_core/u_alu/A
lowercase letters   : 7
digits              : 3
spaces              : 2

Two 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.

Azvya Education Pvt. Ltd.VLSI Mentor
delete-and-squeeze.pl
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
/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

Azvya Education Pvt. Ltd.VLSI Mentor
unequal-lists.pl
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
tr/abcde/xy/ : xyyyy   <- y reused for c, d, e
original : u_alu/A
copy /r  : u_alu_A

Five 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.

Azvya Education Pvt. Ltd.VLSI Mentor
normalise.pl
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);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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    : 3

Two 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 where s/./X/ changed the first character — neither warned.
  • \d in a tr/// list is the two characters \ and d, and Perl says Unrecognized escape \d passed through on 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 \Q to 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.
  • /d deletes the search characters. tr{/}{} counts; tr{/}{}d deletes.
  • /s squeezes runs of the same output character into one, which is the whitespace normaliser.
  • A shorter replacement list reuses its last character, silently.
  • /r returns 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.