Best way to go to any line number we want in Perl

Viewed 379

For an example I am reading file this where after some words I check how many "you" are present.

Good 
morning
to
you
May
every
step
you
make
you
be
filled
you
with
happiness
love
you
and
peace
you

Code I wrote:

use warnings;
use strict;

my $log1_file      = "log.log";
my $you_count      = 0;
my $you_make_count = 0;
my $you_love_count = 0;
my $point ;

open(IN1, "<$log1_file" ) or die "Could not open file $log1_file: $!";
while (my $line = <IN1>) {
    $point =$.;
    print "$. main while\n";
    my @fields = split' ',$line;
   
    if ($fields[0] eq "Good") {
        print "$. after good_if\n";
        good_check();
        print "$. after good_call\n";
        seek (IN1,$point,0);
        #$. = $point;
        print "$. \n";
    }  
    elsif ($fields[0] eq  "make") {
        print "$. after make_if\n";
        make_check();
        #$. = $point;
        seek (IN1,$point,0);
    }
    elsif ($fields[0] eq  "love") {
        print "$. after love_if\n";
        love_check();
        #$. = $point;
        seek (IN1,$point,0);
    }
}

print "$you_count\n";
print "$you_make_count\n";
print "$you_love_count\n";
close IN1;
   
sub love_check{
    while (my $line = <IN1>) 
        my @fields = split' ',$line;
        if ($fields[0] eq "you") {
            $you_love_count++;
        }
    }
}       
    
sub make_check{
    while (my $line = <IN1>) {   
        my @fields = split' ',$line;        
        if ($fields[0] eq "you") {
            $you_make_count++;
        }
    }
}
 
sub good_check{
    while (my $line = <IN1>) {
        my @fields = split' ',$line;
        if ($fields[0] eq "you") {
            $you_count++;
        }
    }
}

If I use seek (IN1,$point,0); to point back to location I am getting output like below:

1 main while
1 after good_if
20 after good_call
20 
21 main while
22 main while
23 main while
24 main while
25 main while
26 main while
27 main while
28 main while
29 main while
29 after make_if
41 main while
42 main while
43 main while
44 main while
44 after make_if
56 main while
Use of uninitialized value $fields[0] in string eq at check.pl line 15, <IN1> line 56.
Use of uninitialized value $fields[0] in string eq at check.pl line 25, <IN1> line 56.
Use of uninitialized value $fields[0] in string eq at check.pl line 33, <IN1> line 56.
57 main while
58 main while
59 main while
60 main while
61 main while
62 main while
63 main while
63 after love_if
68 main while
69 main while
70 main while
70 after love_if
75 main while
76 main while
76 after love_if
81 main while
82 main while
82 after love_if
87 main while
Use of uninitialized value $fields[0] in string eq at check.pl line 15, <IN1> line 87.
Use of uninitialized value $fields[0] in string eq at check.pl line 25, <IN1> line 87.
Use of uninitialized value $fields[0] in string eq at check.pl line 33, <IN1> line 87.
88 main while
89 main while
90 main while
91 main while
6
8
8

The "you" values at final are correct but not getting lines numbers as normal. And if I use $. = $point; only first sub is working fine.

Can any one tell me best way to point back same location?

2 Answers

This question very much seems as though an XY-problem. Or homework. The logic of counting "you"s connected to certain key words seems arbitrary. The "you"s following "Good", for example, will contain all the "you"s in the other words combined.

As I assume this is some sort of learning exercise, I will comment your code, then come with a suggested solution.

open(IN1, "<$log1_file" ) or die "Could not open file $log1_file: $!";

Always use the three argument open with explicit open mode to avoid code injection. Use a lexical file handle (my $fh) instead of a global bareword (IN1). It should look like this:

open my $fh, "<", $log1_file or die "Could not open '$log1_file': $!";

This split is unnecessary

my @fields = split' ',$line;

Since you only have one word on each line, all this does is remove the newline on the end (because splitting on ' ' is a special case). If you want to remove the newline, you should use chomp, like this: chomp($line)

Using seek and tell to navigate around your file is likely the wrong solution. While you can make it work, there are better solutions.

Using three almost identical subroutines to do the exact same thing (almost) is bad practice, IMO. Using global variables inside the subroutines is also not a good thing. What you should seek is encapsulation: Feed the information you need to the subroutine, and return the values you want afterwards. For example:

my @file = <$fh>     # slurp the file into an array
....
if (/^Good$/) {
    $good_count += you_check($line_number);
} elsif (/^make$/) {
    $make_count += you_check($line_number);
} ....etc

sub you_check {
    my $line_number = shift;
    my $count = 0;
    for my $line ($file[$line_number] .. $file[$#file]) {
        $count++ if $line =~ /^you$/;
    }
    return $count;
}

Assuming we keep @file unchanged, the you_check() function can be used without worrying that using it will alter something else.

With that said, if I was to solve this task, I would use a hash. It will allow you to dynamically determine the keyword, and add new keywords without having to add a lot of new code.

use strict;
use warnings;
use Data::Dumper;

my %count;
my $key;
while (<>) {
    if (/(Good|make|love)/) {
        $key = $1;
    }
    if (/you/) {
        $count{$key}++ if $key;
    }
}
print Dumper \%count;

Use it like this on the command line:

$ count.pl log.log

The output when used with your sample data is:

$VAR1 = {
          'love' => 2,
          'Good' => 2,
          'make' => 2
        };

If you still wanted to maintain the rule that the first word count contains all other words, you can keep track of which word comes first and then just add the counts afterwards. Using a hash for this count is scalable for as many words as you like to keep track of.

Files are streams of bytes. They don't understand lines at all. The line-end character is just another character in the stream.

So when the documentation for seek() talks about a "FILEHANDLE's position", it means the number of bytes through the file, not the number of lines. So seeking to a line number is never going to have the result that you want.

Instead, you should use the tell() function to find out which position you are at in the file and then use that value as the parameter that you send to seek().

It would work something like this:

# Lexical filehandle and three-arg open()
open my $in_fh, '<', $log1_file or die ...;

my $pos = tell $in_fh;

while (my $line = <$in_fh>) {

  # Do various stuff.

  # If you want to go back to the start of the line.
  seek $in_fh, $pos, 0;

  # Get the pos at the end of the current line
  $pos = tell $in_fh;
}

Update: If you want to be able to go to any line in a file, then one approach might be to read through the file once and call tell() at the start of every line. You can then store those values in an array which you can use to get to the start of any line.

Related