views:

1704

answers:

5

Is there an elegant way in Perl to find the newest file in a directory (newest by modification date)?

What I have so far is searching for the files I need, and for each one get it's modification time, push into an array containing the filename, modification time, then sort it.

There must be a better way.

+1  A: 

you could try using the shell's ls command:

@list = `ls -t`;
$newest = $list[0];
Nathan Fellman
Only works on UNIX (or Windows with an ls command in, say, Cygwin), but it IS a more elegant solution.
paxdiablo
Perhaps the shortest. Hardly elegant. Alnitak has it right.
Konrad Rudolph
It works on Windows (using gnuwin32 utilities). But `ls -t` returns both file and *directory* names.
J.F. Sebastian
This isn't elegant at all. It's just less typing. Now you need to create a new process for every directory you want to examine. Hardly pretty. :)
brian d foy
+2  A: 

Assuming you know the $DIR you want to look in:

opendir(my $DH, $DIR) or die "Error opening $DIR: $!";
my %files = map { $_ => (stat("$DIR/$_"))[9] } grep(! /^\.\.?$/, readdir($DH));
closedir($DH);
my @sorted_files = sort { $files{$b} <=> $files{$a} } (keys %files);
# $sorted_files[0] is the most-recently modified. If it isn't the actual
# file-of-interest, you can iterate through @sorted_files until you find
# the interesting file(s).

The grep that wraps the readdir filters out the "." and ".." special files in a UNIX(-ish) filesystem.

rjray
Isn't this almost what Bonzo is saying he's doing? "searching for the files I need, and for each one get it's modification time, push into an array containing the filename, modification time, then sort it." You just change the array of tuples for a hash.
Vinko Vrsalovic
+2  A: 

If you can't let ls do the sorting for you as @Nathan suggests, then you can optimize your process by only keeping the newest modification time and associated filename seen thus far and replace it every time you find a newer file in the directory. No need to keep any files around that you know are older than the newest one you've seen so far and certainly no need to sort them since you can detect which is the newest one while reading from the directory.

tvanfosson
+8  A: 

Your way is the "right" way if you need a sorted list (and not just the first, see Brian's answer for that). If you don't fancy writing that code yourself, use this

use File::DirList;
my @list = File::DirList::list('.', 'M');

Personally I wouldn't go with the ls -t method - that involves forking another program and it's not portable. Hardly what I'd call "elegant"!


Regarding rjray's solution hand coded solution, I'd change it slightly:

opendir(my $DH, $DIR) or die "Error opening $DIR: $!";
my @files = map { [ stat "$DIR/$_", $_ ] } grep(! /^\.\.?$/, readdir($DH));
closedir($DH);

sub rev_by_date { $b->[9] <=> $a->[9] }
my @sorted_files = sort rev_by_date @files;

After this, @sorted_files contains the sorted list, where the 0th element is the newest file, and each element itself contains a reference to the results of stat, with the filename itself in the last element:

my @newest = @{$sorted_files[0]};
my $name = pop(@newest);

The advantage of this is that it's easier to change the sorting method later, if desired.


EDIT: here's an easier-to-read (but longer) version of the directory scan, which also ensures that only plain files are added to the listing:

my @files;
opendir(my $DH, $DIR) or die "Error opening $DIR: $!";
while (defined (my $file = readdir($DH))) {
  my $path = $DIR . '/' . $file;
  next unless (-f $path);           # ignore non-files - automatically does . and ..
  push(@files, [ stat(_), $path ]); # re-uses the stat results from '-f'
}
closedir($DH);

NB: the test for defined() on the result of readdir() is because a file called '0' would cause the loop to fail if you only test for if (my $file = readdir($DH))

Alnitak
Both `File::DirList` and `ls` requires installation (on Windows at least). `
J.F. Sebastian
If you want *newest* then use `@l = File::DirList::list('.', 'M'); say $l[0][0][13]` Note the capital M.
J.F. Sebastian
I wouldn't call his the "right" way. It's going to be a slug for a directory with many files.
brian d foy
Both `File::DirList::list` and `ls -t` return both filenames and *dirnames*.
J.F. Sebastian
brian - no more so that calling 'ls'. JFS - my new Perl version excludes dirnames.
Alnitak
You don't want to make paths like that. Use File::Spec to help you.
brian d foy
Brian - these are examples of _some_ improvements on another poster's example, not a howto on "perfect" portable coding. It would have been better to say _why_ you've changed your own example rather than just change it and delete the comments.
Alnitak
+4  A: 

You don't need to keep all of the modification times and filenames in a list, and you probably shouldn't. All you need to do is look at one file and see if it's older than the oldest you've previously seen:

{
opendir my( $dh ), $dir or die "Could not open $dir: $!";

my( $newest_name, $newest_time );

while( defined( my $file = readdir( $dh ) ) )
    {
    my $path = File::Spec->catfile( $dir, $file );
    ( $newest_name, $newest_time ) = ( $file, -M _ ) if -M $path < $newest_time;
    }

print "Newest file is $newest_name\n";
}
brian d foy
It doesn't filter directories names.
J.F. Sebastian