rbaumann
rbaumann

Reputation: 129

Perl tr operator is transliterating based on the variable's name not its value

I'm using Perl 5.16.2 to try to count the number of occurrences of a particular delimiter in the $_ string. The delimiter is passed to my Perl program via the @ARGV array. I verify that it is correct within the program. My instruction to count the number of delimiters in the string is:

$dlm_count = tr/$dlm//;

If I hardcode the delimiter, e.g. $dlm_count = tr/,//; the count comes out correctly. But when I use the variable $dlm, the count is wrong. I modified the instruction to say

$dlm_count = tr/$dlm/\t/;

and realized from how the tabs were inserted in the string that the operation was substituting every instance of any of the four characters "$", "d", "l", or "m" to \t — i.e. any of the four characters that made up my variable name $dlm.

Here is a sample program that illustrates the problem:

$_ = "abcdefghij,klm,nopqrstuvwxyz";
my $dlm = ",";
my $dlm_count = tr/$dlm/\t/;
print "The count is $dlm_count\n";
print "The modified string is $_\n";

There are only two commas in the $_ string, but this program prints the following:

The count is 3  
The modified string is abc      efghij,k                ,nopqrstuvwxyz

Why is the $dlm token being treated as a literal string of four characters instead of as a variable name?

Upvotes: 1

Views: 656

Answers (4)

zdim
zdim

Reputation: 66899

You cannot use tr that way, it doesn't interpolate variables. It runs strictly character by character replacement. So this

$string =~ tr/a$v/123/

is going to replace every a with 1, every $ with 2, and every v with 3. It is not a regex but a transliteration. From perlop

Because the transliteration table is built at compile time, neither the SEARCHLIST nor the REPLACEMENTLIST are subjected to double quote interpolation. That means that if you want to use variables, you must use an eval():

eval "tr/$oldlist/$newlist/";
die $@ if $@;
eval "tr/$oldlist/$newlist/, 1" or die $@;

The above example from docs hints how to count. For $dlms in $string

$dlm_count = eval "\$string =~ tr/$dlm//";  

The $string is escaped so to not be interpolated before it gets to eval. In your case

$dlm_count = eval "tr/$dlm//";

You can also use tools other than tr (or regex). For example, with string being in $_

my $dlm_count = grep { /$dlm/ } split //;

When split breaks $_ by the pattern that is empty string (//) it returns the list of all characters in it. Then the grep block tests each against $dlm so returning the list of as many $dlm characters as there were in $_. Since this is assigned to a scalar, $dlm_count is set to the length of that list, which is the count of all $dlm.

Upvotes: 3

Chris Charley
Chris Charley

Reputation: 6613

In the section of the docs on perlop 'Quote Like Operators', it states:

Because the transliteration table is built at compile time, neither the SEARCHLIST nor the REPLACEMENTLIST are subjected to double quote interpolation. That means that if you want to use variables, you must use an eval():

Upvotes: 3

ikegami
ikegami

Reputation: 386396

As documented and as you discovered, tr/// doesn't interpolate. The simple solution is to use s/// instead.

my $dlm = ",";
$_ = "abcdefghij,klm,nopqrstuvwxyz";
my $dlm_count = s/\Q$dlm/\t/g;

If the transliteration is being performed in a loop, the following might speed things up noticeably:

my $dlm = ",";
my $tr = eval "sub { tr/\Q$dlm\E/\\t/ }";
for (...) {
   my $dlm_count = $tr->();
   ...
}

Upvotes: 2

cdlane
cdlane

Reputation: 41905

Although several answers have hinted at the eval() idiom for tr///, none have the form that covers cases where the string has tr syntax characters in it, e.g.- (hyphen):

$_ = "abcdefghij,klm,nopqrstuvwxyz";

my $dlm = ",";

my $dlm_count = eval sprintf "tr/%s/%s/", map quotemeta, $dlm, "\t";

But as others have noted, there are lots of ways to count characters in Perl that avoid eval(), here's another:

my $dlm_count = () = m/$dlm/go;

Upvotes: 1

Related Questions