techie
techie

Reputation: 59

Opening a directory and searching for a pattern in files

My Perl script is supposed to open all the files in a directory and search for a pattern inside them and print the whole line containing the pattern. Below is the code. The code is unable to open the file.

  my $dir = 'dir1/dir2';
    opendir (DIR, $dir) or die $!;
    my @dir = readdir DIR;
    foreach my $item (@dir) {
        open(FILE, "<", "file.txt")
        or die "Can't open < file.txt: $!";
        while($line= <FILE>) {
        print "$line" if $line=~ /pattern/;
    }
    close FILE;
    }
    closedir DIR;

Please suggest some way to make it work.

Upvotes: 1

Views: 801

Answers (1)

zdim
zdim

Reputation: 66883

The readdir returns just names (file.txt), not paths (not dir1/dir2/path.txt).

So when the program tries to open $item it turns out that there is no such a thing -- need to open "$dir/$item". Or, prepend that to every entry in the filelist and then work away with correct paths

use warnings;
use strict;

my $dir = 'dir1/dir2';

opendir (my $dh, $dir) or die $!;
my @entries = map { "$dir/$_" } readdir $dh;
closedir $dh;

foreach my $item (@entries) {
    next if not -f $item;
    open(my $fh, "<", $item) or die "Can't open < $item: $!";

    while(my $line = <$fh>) {
        print $line if $line =~ /pattern/;
    }
    close $fh;
}

Note that I skip entries which aren't regular files (not -f, see filetest operators), and use lexical filehandes instead of typeglobs (my $fh instead of FILE).

Another option for getting lists of entries is glob (see File::Glob), which does return the path:

my @entries = glob $dir;  # dir1/dir2/file.txt  (etc)

But if you are indeed acquiring the whole filelist ahead, as opposed to reading and processing an item at a time, then you might as well filter it as needed right away, so

my @files = grep { -f } glob $dir;

where you now have only regular files.

The whole list can be filtered also in the program above of course. Can do it in the map itself

opendir (my $dh, $dir) or die $!;
my @files = map { -f "$dir/$_" ? "$dir/$_" : () } readdir $dh;
closedir $dh;

Here map works as a filter as well by a little trick: An empty list (), returned when an entry isn't a plain file, gets flattened in the returned list thus effectively disappearing, so this amounts to not returning anything in that case. Or, chain grep to map as usual

my @files = grep { -f } map { "$dir/$_" } readdir $dh;

This does make (yet) another pass over the list, but that is optimized and hardly ever a concern (unless we had huge filelists -- what poses problems in its own right -- and were doing this very often).


I'd like to also mention that there are good modules for this, as well.

An example with Path::Tiny

use Path::Tiny;

my $it = path($dir)->iterator;

while (my $entry = $it->()) { 
    # it omits . and ..
}

This is a "lazy" iterator. See docs, and more so for the many other things that this module offers.


Instead of that file.txt

Upvotes: 2

Related Questions