Reputation: 177
Here is my code:
#!perl -w
use strict;
my %hash = (
1 => "a",
2 => "b",
);
foreach my $num ( keys %hash ) {
while (<DATA>) {
s/$num/$hash{$num}/g;
print;
}
}
__DATA__
121212
11111
222
I intend to replace all numbers with the corresponding values that exists in hash. But it outputs:
a2a2a2
aaaaa
222
Hit any key to close this window...
Why is the foreach loop run once only? Who can explain it for me? And how should I change the code? I want it to output:
ababab
aaaaa
bbb
Thanks in advance.
Upvotes: 3
Views: 573
Reputation: 8532
The simplest answer is simply to do the foreach
inside the while
, as
#!perl -w
use strict;
my %hash = (
1 => "a",
2 => "b",
);
while (<DATA>) {
foreach my $num ( keys %hash ) {
s/$num/$hash{$num}/g;
}
print;
}
__DATA__
121212
11111
222
As others have pointed out, the problem with the original program was the read position of the DATA
filehandle. The rewind
function is normally used to reset the position of a filehandle back to the beginning so it can be read again, but in the specific case of DATA
it doesn't work properly, because of the way that the handle is really just perl's internal filehandle on the .pl
file itself. However, you can save the position using tell
and then reset back there for each iteration using seek
:
#!perl -w
use strict;
use Fcntl qw( SEEK_SET );
my %hash = (
1 => "a",
2 => "b",
);
my $pos = tell DATA;
foreach my $num ( keys %hash ) {
seek DATA, $pos, SEEK_SET;
while (<DATA>) {
s/$num/$hash{$num}/g;
print;
}
}
__DATA__
121212
11111
222
Gives the output:
a2a2a2
aaaaa
222
1b1b1b
11111
bbb
Upvotes: 0
Reputation: 67900
The problem with your code is what Benj points out, that the DATA
file handle is at eof for the second iteration of foreach
. But also that you print the values during the first iteration.
If you are going to replace only one character for another, i.e. the length of your key/values are never longer than 1, I think you should use tr///
instead.
use strict;
use warnings;
while (<DATA>) {
tr/12/ab/;
print;
}
__DATA__
121212
11111
222
The transliteration operator will replace all the characters in the string in one go. The one problem is that the tr///
operator must be hard coded, so no dynamic switching of characters.
If you want to use a hash, you should take Sinan's caution to heart if your key/values can be longer than 1. For example, if you have both keys 1
and 11
, which should take precedence over the other? However, if it is single character replacements, this is a way to do it smoothly:
my %hash = qw(1 a 2 b);
while (<DATA>) {
s|(.)| $hash{$1} // $1 |ge;
print;
}
Note the use of the regex modifier /e
, it makes the substitution evaluate the RHS and insert the return value. In this case, we make use of the //
operator. What it does is check if the hash value is defined, and if not, just use the key instead. Similar to
if (defined $hash{$1}) {
$hash{$1};
} else {
$1;
}
Or
defined $hash{$1} ? $hash{$1} : $1
Note also that since it takes one character at the time with (.)
, it will only work for single character keys.
Upvotes: 1
Reputation: 118128
Instead of looping through the file for each key in the hash, or slurping the file, you can combine the keys of the hash into one search pattern.
Note that in the current version of your script as well as in @Benj's answer, the order in which the substitutions are applied is indeterminate in that the ordering of keys returned by keys can differ across perl
s or even across different runs using the same perl
.
Which means you should pick an ordering, and stick with it unless you like surprises. The following script combines both ideas. I decided to put longer keys before shorter ones which makes sense in most contexts.
#!perl -w
use strict;
my %hash = qw(1 a 11 zz 2 b);
my $pat = join '|',
map qr/\Q$_\E/,
sort { length $b <=> length $a }
keys %hash
;
while (<DATA>) {
s/($pat)/$hash{$1}/g;
print;
}
__DATA__
121212
11111
222
Output:
ababab zzzza bbb
Upvotes: 1
Reputation: 32398
The reason the foreach
loop appears to run only once is because you're reading from <DATA>
one line at a time. The second time you loop through the outer loop, there's no more data left to read from data in your inner loop. Instead why don't you read all the <DATA>
into a list first:
@mylist = <DATA>
And then loop over this list in your inner loop.
Upvotes: 4