BrowserUk has asked for the wisdom of the Perl Monks concerning the following question:

I currently am using the line

my @dirs = grep { -d "$base/$_" and $_ ne "." and $_ ne ".." } readdir +(DIR);
to build a list of dirs from the return of readdir. What I now wish to do is build a second list of just the files from the same readdir list. In keeping with Swartzian principle, what I want to write is something like:

my (@dirs,@files) = grep { -d "$base/$_" and $_ ne "." and $_ ne ".." } { -f "$base/$_" } readdir(DIR);
I realise that I can do:

my @all = readdir(DIR); my @dirs = grep {-d "$base/£_" and $ne "." and $_ ne ".." } @all; my @files = grep {-f "$base/£_" } @all;

But my instinct say (probably wrongly) that this can be done without the need to name @all? Thanks.

Replies are listed 'Best First'.
Re: Producing 2 lists from a grep call
by merlyn (Sage) on Jun 16, 2002 at 19:52 UTC
    Applying the KISS principle:
    my @dirs; my @files; for (readdir DIR) { next if $_ eq "." or $_ eq ".."; if (-d "$base/$_") { push @dirs, $_; } else { push @files, $_; } }
    Don't try to cooerce everything into a grep.

    -- Randal L. Schwartz, Perl hacker

      Run that code on /dev and it will get seriously wrong results.

      A file isn't just anything that is not a directory.

Re: Producing 2 lists from a grep call
by dws (Chancellor) on Jun 16, 2002 at 19:52 UTC
    What you've got works, but if you're concerned about space overhead, you could do something like
    foreach my $file ( readdir(DIR) ) { push @dirs, $file if -d "$base/$file" and $file ne "." and $file ne ".."; push @files, $file if -f "$base/$file"; }
Re: Producing 2 lists from a grep call
by tstock (Curate) on Jun 16, 2002 at 20:03 UTC
    You can use a while loop around the readdir call, and push the files into your @files, @dirs arrays using a if, elsif, else construct. no need for intermediary @all.

    If you don't like this solution for being too simple or verbose, then you could always play with map in void context and the ? : switches.

      No need for a void map.
      for (readdir DIR) { next if $_ eq "." or $_ eq ".."; next unless -d "$base/$_" or -f _; push @{ -d _ ? \@dirs : \@files }, $_; }
      Update: Excellent point by particle++. Code updated accordingly.

      Makeshifts last the longest.

        I quite like that one. I came close to that but missed (actually, simply wasn't aware of) the @{\@array) 'trick'.

        However (you knew that was coming right?), as I can do:

        my (@a1,@a2) = ([1,2,3],[3,2,1]);

        Update: the above line doesn't work after all

        I had tried:

        my (@a1,@a2) = ([1,2,3,4,5,6,7],[7,6,5,4,3,2,1]); print Dumper(@a1), Dumper(@a2);

        which gave:

        $VAR1 = [ 1, 2, 3, 4, 5, 6, 7 ]; $VAR2 = [ 7, 6, 5, 4, 3, 2, 1 ];
        which certainly lookedlike it had worked! It was only after seeing this that I went back and added a couple of labels

        print "a1\n", Dumper(@a1), "a2\n", Dumper(@a2);

        That I saw what tstock meant below.

        Thanks tstock, I'll be more careful in future with my quick tests. (I agree about the loop/if being better too.)

        end of update

        Which personally I find a very clear yet concise way of declaring and initialising two arrays. It still seems as if something close to:

        my ( @dirs, @files)= ( @{-d}, @{-f} ) for (readdir DIR);

        could be possible if I could only get the syntactic sugar right!

        This is only an exercise in my trying to understand arrays and list contexts etc. ie. Its essentially a purely academic excercise for late (for me) on a Sunday evening, and so not worthy of anyone's time unless they are also in play mode.

        this code does not work -- !-d  !=  -f!

        to implement this code properly, you must test for both -d, and -f, and ignore the rest. add next unless -d $_ or -f _; before the push statement and you'll have what you're looking for.

        for (readdir DIR) { next if $_ eq '.' or $_ eq '..'; next unless -d $base . '/' . $_ or -f _; push @{ -d $base . '/' . $_ ? \@dirs : \@files }, $_; }
        Update: changes as per Screamer

        Update 2: for more info on filetests, see -X under perlfunc

        ~Particle *accelerates*

Re: Producing 2 lists from a grep call
by marvell (Pilgrim) on Jun 17, 2002 at 12:29 UTC
    How about something that is like the following pseudo code:
    for $item @list { next if definite failure condition action if most likely condition action if next most likely condition etc. default action }
    Giving you:
    for (readdir DIR) { next if $_ eq '.' || $_ eq '..'; push (@files,$_) && next if -f $base/$_; push (@dirs,$_) && next if -d $base/$_; # default to no operation }
    I imagine this will look a lot more elegant with the new perl 6 switch statement.

    Steve Marvell