<!-- category: hidden-gems -->
File Age in Fractional Days
How old is this file? The hard way:The Perl way:my @stat = stat('script.pl'); my $age_seconds = time() - $stat[9]; my $age_days = $age_seconds / 86400;
One operator. No stat call. No date math. No dividing by 86400. Justmy $age_days = -M 'script.pl';
-M and a filename. It returns the file's age in fractional days since your script started running.
Was the file modified in the last hour? -M $file < (1/24). In the last 5 minutes? -M $file < (5/1440). Older than a week? -M $file > 7.
This is one of Perl's file test operators, and it's criminally underused.
Part 1: WHAT -M RETURNS
-M returns the number of days (as a floating-point number) since the file was last modified, measured from when your script started running.
The reference point ismy $age = -M '/var/log/syslog'; say $age; # something like 0.0834 (about 2 hours)
$^T, the time the script began. This means every -M call in the same script uses the same baseline. Files don't appear to age during execution.
Fractional days sound weird, but they make comparisons trivial. "Less than 1" means today. "Greater than 7" means more than a week old. No unit conversion needed.VALUE MEANING -------- ---------------------------- 0.0007 modified ~1 minute ago 0.0417 modified ~1 hour ago 0.5 modified 12 hours ago 1.0 modified exactly 1 day ago 7.0 modified 1 week ago 30.5 modified about a month ago
Part 2: -A AND -C
-M has two siblings:
-M is when the file's content was last changed. The one you almost always want.my $mod_age = -M $file; # modification time (mtime) my $access_age = -A $file; # access time (atime) my $change_age = -C $file; # inode change time (ctime)
-A is when the file was last read. Useful for finding files nobody has touched in a while. Note: many filesystems mount with noatime for performance, which makes -A unreliable.
-C is when the file's metadata changed. Permissions, ownership, hard links. On Unix, this is the inode change time. It's not "creation time" even though the letter suggests it.
# find files that were read more recently than they were modified # (someone looked at it but didn't edit it) if (-A $file < -M $file) { say "$file was read since its last edit"; }
Part 3: PRACTICAL AGE CHECKS
The real power is in simple comparisons. No date parsing. No DateTime objects. Just numbers.Modified today (within last 24 hours):
Modified in the last hour:if (-M $file < 1) { say "$file was modified today"; }
Modified in the last 5 minutes:if (-M $file < 1/24) { say "$file was modified in the last hour"; }
Older than 30 days:if (-M $file < 5/1440) { say "$file is fresh (< 5 min)"; }
Between 1 and 7 days old:if (-M $file > 30) { say "$file is stale"; }
Nomy $age = -M $file; if ($age >= 1 && $age <= 7) { say "$file was modified this week"; }
stat(). No localtime(). No POSIX::strftime(). Just a number and a comparison.
Part 4: CACHE INVALIDATION
Here's where-M shines in real code. You have a cache file. If it's fresh enough, use it. If it's stale, rebuild it.
Two lines of cache logic:my $cache = '/tmp/myapp_cache.json'; if (-e $cache && -M $cache < 1/24) { # cache is less than 1 hour old, use it open my $fh, '<', $cache or die "Cannot read $cache: $!\n"; my $data = do { local $/; <$fh> }; close $fh; return decode_json($data); } else { # cache is stale or missing, rebuild my $data = fetch_fresh_data(); open my $fh, '>', $cache or die "Cannot write $cache: $!\n"; print $fh encode_json($data); close $fh; return $data; }
-e $cache && -M $cache < 1/24. That's the entire invalidation strategy. No timestamp columns. No TTL calculations. The filesystem is the clock.
Part 5: LOG ROTATION CHECKS
Sysadmin scripts love-M. Check if logs need rotation:
That walks a log directory and flags anything older than a week or bigger than 100 MB. The#!/usr/bin/env perl use strict; use warnings; use feature 'say'; my @logs = glob('/var/log/myapp/*.log'); for my $log (@logs) { my $age = -M $log; my $size = -s $log; if ($age > 7 || $size > 100_000_000) { say sprintf("ROTATE: %-40s age=%.1f days size=%d MB", $log, $age, $size / 1_000_000); } }
-M and -s operators make the logic almost pseudocode.
Part 6: FILE TEST STACKING AND THE _ CACHE
Perl caches the result of the last file test in a special filehandle called_ (underscore). You can chain tests without hitting the filesystem multiple times:
The first testif (-f $file && -r _ && -M _ < 1) { say "$file is a regular, readable file modified today"; }
-f $file does the actual stat() call and caches the result. The subsequent -r _ and -M _ reuse that cached stat data. Three tests, one system call.
This matters when you're scanning thousands of files:
Without themy @recent_readable = grep { -f $_ && -r _ && -M _ < 7 } @files;
_ cache, that's three stat() calls per file. With it, one.
Starting in Perl 5.10, you can also stack file tests directly:
Butif (-f -r -w $file) { say "$file is a regular, readable, writable file"; }
-M can't participate in this stacked syntax because it returns a number, not a boolean. You still need to use _ for combining -M with other tests:
if (-f $file && -M _ < 1) { say "regular file, modified today"; }
Part 7: FINDING RECENT FILES
Scan a directory for files modified in the last N days:The sort at the end orders by age, most recently modified first.sub find_recent { my ($dir, $max_age_days) = @_; $max_age_days //= 1; opendir my $dh, $dir or die "Cannot open $dir: $!\n"; my @recent; while (my $entry = readdir $dh) { next if $entry =~ m~^\.~; my $path = "$dir/$entry"; next unless -f $path; push @recent, $path if -M _ < $max_age_days; } closedir $dh; return sort { (-M $a) <=> (-M $b) } @recent; } my @today = find_recent('/var/log', 1); say $_ for @today;
-M in a sort comparator is perfectly valid.
Part 8: COMPARISON TO STAT()
Here's the same logic withstat():
The# with stat() my $mtime = (stat $file)[9]; my $age_days = (time() - $mtime) / 86400; if ($age_days < 1) { ... } # with -M if (-M $file < 1) { ... }
stat() version is five tokens. The -M version is five tokens too, but it's one expression instead of three statements. No intermediate variables. No magic number 86400.
There is one important difference. -M measures from $^T (script start time), not from time() (current time). For short-running scripts, the difference is negligible. For long-running daemons, it matters.
If you need age relative to the current moment in a long-running process:
Or just use# reset the baseline to now $^T = time(); my $age = -M $file; # now measures from this moment
stat():
For scripts that run and exit (the vast majority),my $age = (time() - (stat $file)[9]) / 86400;
-M is simpler and cleaner.
Part 9: CLEANUP SCRIPTS
The classic sysadmin pattern. Delete temp files older than N days:The#!/usr/bin/env perl use strict; use warnings; use feature 'say'; my $dir = '/tmp/myapp'; my $max_age = 7; # days my $dry_run = 1; opendir my $dh, $dir or die "Cannot open $dir: $!\n"; while (my $file = readdir $dh) { my $path = "$dir/$file"; next unless -f $path; if (-M _ > $max_age) { if ($dry_run) { say sprintf("WOULD DELETE: %s (%.1f days old)", $path, -M _); } else { unlink $path or warn "Cannot delete $path: $!\n"; say "DELETED: $path"; } } } closedir $dh;
-M _ > $max_age is the entire decision. The _ reuses the stat from the -f test above it. Clean, efficient, obvious.
Part 10: NEGATIVE VALUES
Here's a weird one.-M can return negative numbers:
A negative value means the file's modification time is after# touch a file with a future timestamp system('touch', '-t', '203001010000', '/tmp/future.txt'); say -M '/tmp/future.txt'; # negative! (file is from "the future")
$^T. This can happen with:
- Files with future timestamps (set manually or by buggy software)
- Clock skew on NFS mounts
- Timezone confusion
It's rare, but if you're doing automated age checks, guard against it:
Thatmy $age = -M $file; if ($age >= 0 && $age < 1) { say "Modified today"; }
>= 0 protects you from future-dated files sneaking through.
.--. |o_o | "stat() is 10 characters. |:_/ | -M is 2." // \ \ (| | ) /'\_ _/`\ \___)=(___/ -M modification age -A access age -C inode change age Returns fractional days. 1 = yesterday. 0.5 = 12 hours ago. 7 = a week.
-M is one of those operators that makes you realize how much Perl was designed by someone who actually wrote sysadmin scripts. Checking file age shouldn't require three lines of code and a division by 86400. It should be two characters and a comparison.
File age in days. No imports. No date math. No stat unpacking. Just -M and a number. If you're writing any script that touches the filesystem, this operator will save you time on every single file test.
perl.gg