Michael Kjörling Michael Kjörling - 11 months ago 59
Perl Question

Find out whether a file newer than a given date/time exists in a directory?

Let's say I have a list of directories, each of which may or may not contain subdirectories which I also want to consider.

Let's also say I have a list of timestamps, one for each directory in the list (but not subdirectories). These are known as date and time with an implicit time zone, so can fairly easily be converted to a Unix timestamp if that makes it easier to compare.

How do I, for each of those directories listed, find out whether there exists any file within the directory which is newer (in terms of mtime or ctime, but not atime) than the timestamp that I have for the directory in question?

I'm not really interested in which specific file is newer than the timestamp, only whether or not any such files exist at all.

Basically, I want to write a script that when run performs a specific action if any file inside any one of a set of directories has been changed after a given point in time, and need to come up with a way to detect whether anything has been changed.

Answer Source

Your problem can be translated into multiple easy subproblems

  1. Q: How do I recursively look at each file inside a directory?

    A: use File::Find. This would look a bit like

    use File::Find;
    find sub {
      return unless -f;
      if (file_is_newer_than($timestamp)) {
        do something;
    }, $top_dir;
  2. Q: How do I do this for multiple directories?

    A: Wrap it in a foreach loop, e.g.

    for my $dir_time (["./foo", 1234567890], ["./bar", 1230987654]) {
      my ($top_dir, $timestamp) = @$dir_time;
      # above code
  3. Q: How do I determine if the file is newer?

    A: stat it for mtime or ctime, then compare result with your timestamp. E.g.

    use File::stat;
    say "$_ is new!" if stat($_)->mtime > $timestamp;
  4. Q: I'm only interested whether or not any such files exist at all. How can I short curcuit the find?

    A: Tricky one. We can't just return from the find, because that would just exit from the coderef we passed it. Instead, we can use the exceptions-for-control-flow antipattern:

    eval {
      find {
        wanted => sub {
          return unless -f;
          die "New file found\n" if stat($_)->mtime > $timestamp;
        no_chdir => 1,
      } $top_dir;
    if ($@) {
      # I should really use exception objects hereā€¦
      if ($@ eq "New file found\n") {
        say "New file in $top_dir found";
      } else {
        die $@;  # rethrow error

    I set the no_chdir option so that I don't have to restore the correct working directory in the exception handler.

    Or we could use loop control on labeled blocks:

    DIR: for my $dir_time (...) {
      my ($top_dir, $timestamp) = @$dir_time;
      RECURSION: {   
        find {
          wanted => sub {
            return unless -f;
            last RECURSION if stat($_)->mtime > $timestamp; # exit the RECURSION block
          no_chdir => 1,
        } $top_dir;
        # if we are here, no newer file was found.
        next DIR; # make sure to skip over below code; go to next iteration
      # this code only reached when a newer file was found
      say "New file found";

    While this doesn't abuse exceptions for control flow, this will trigger warnings:

    Exiting subroutine via last

    We can silence this with no warnings 'exiting'.

NB: All code in here is quite untested.