I quickly jotted off a Perl script that would average a few files with just columns of numbers. It involves reading from an array of filehandles. Here is the script:
#!/usr/local/bin/perl
use strict;
use warnings;
use Symbol;
die "Usage: $0 file1 [file2 ...]\n" unless scalar(@ARGV);
my @fhs;
foreach(@ARGV){
my $fh = gensym;
open $fh, $_ or die "Unable to open \"$_\"";
push(@fhs, $fh);
}
while (scalar(@fhs)){
my ($result, $n, $a, $i) = (0,0,0,0);
while ($i <= $#fhs){
if ($a = <$fhs[$i]>){
$result += $a;
$n++;
$i++;
}
else{
$fhs[$i]->close;
splice(@fhs,$i,1);
}
}
if ($n){ print $result/$n . "\n"; }
}
This doesn't work. If I debug the script, after I initialize @fhs it looks like this:
DB<1> x @fhs
0 GLOB(0x10443d80)
-> *Symbol::GEN0
FileHandle({*Symbol::GEN0}) => fileno(6)
1 GLOB(0x10443e60)
-> *Symbol::GEN1
FileHandle({*Symbol::GEN1}) => fileno(7)
So far, so good. But it fails at the part where I try to read from the file:
DB<3> x $fhs[$i]
0 GLOB(0x10443d80)
-> *Symbol::GEN0
FileHandle({*Symbol::GEN0}) => fileno(6)
DB<4> x $a
0 'GLOB(0x10443d80)'
$a is filled with this string rather than something read from the glob. What have I done wrong?
If we have a reference to an array we can also easily access the individual elements. If we have the array @names we access the first element using $names[0]. That is, we replace the @-sign with a $-sign and put the index in square brackets after the name. With references we do the same.
To access a single element of a Perl array, use ($) sign before variable name. You can assume that $ sign represents singular value and @ sign represents plural values. Variable name will be followed by square brackets with index number inside it. Indexing will start with 0 from left side and with -1 from right side.
You can only use a simple scalar variable inside <>
to read from a filehandle. <$foo>
works. <$foo[0]>
does not read from a filehandle; it's actually equivalent to glob($foo[0])
. You'll have to use the readline
builtin, a temporary variable, or use IO::File and OO notation.
$text = readline($foo[0]);
# or
my $fh = $foo[0]; $text = <$fh>;
# or
$text = $foo[0]->getline; # If using IO::File
If you weren't deleting elements from the array inside the loop, you could easily use a temporary variable by changing your while
loop to a foreach
loop.
Personally, I think using gensym
to create filehandles is an ugly hack. You should either use IO::File, or pass an undefined variable to open
(which requires at least Perl 5.6.0, but that's almost 10 years old now). (Just say my $fh;
instead of my $fh = gensym;
, and Perl will automatically create a new filehandle and store it in $fh
when you call open
.)
If you are willing to use a bit of magic, you can do this very simply:
use strict;
use warnings;
die "Usage: $0 file1 [file2 ...]\n" unless @ARGV;
my $sum = 0;
# The current filehandle is aliased to ARGV
while (<>) {
$sum += $_;
}
continue {
# We have finished a file:
if( eof ARGV ) {
# $. is the current line number.
print $sum/$. , "\n" if $.;
$sum = 0;
# Closing ARGV resets $. because ARGV is
# implicitly reopened for the next file.
close ARGV;
}
}
Unless you are using a very old perl, the messing about with gensym
is not necessary. IIRC, perl 5.6 and newer are happy with normal lexical handles: open my $fh, '<', 'foo';
I have trouble understanding your logic. Do you want to read several files, which just contains numbers (one number per line) and print its average?
use strict;
use warnings;
my @fh;
foreach my $f (@ARGV) {
open(my $fh, '<', $f) or die "Cannot open $f: $!";
push @fh, $fh;
}
foreach my $fh (@fh) {
my ($sum, $n) = (0, 0);
while (<$fh>) {
$sum += $_;
$n++;
}
print "$sum / $n: ", $sum / $n, "\n" if $n;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With