Reputation: 129
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
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 $dlm
s 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
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
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
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