5

I want to display time in the same format as is displayed by the linux diff command.

D:\>diff -dub old.cfg new.cfg
--- old.cfg       2019-05-15 15:03:14.289950700 +0530
+++ new.cfg       2019-05-14 16:07:21.119695300 +0530

I want to show last modified time of a file as 2019-05-15 15:03:14.289950700 +0530.
I used stat but I am not getting that nanosecond part. Alo, need timezone in given format. I tried Time::HiRes too, but couldn't get.

my $dt = strftime("%Y-%m-%d %H:%M:%S", localtime((stat($old_file))[9]));

It returns: 2019-05-15 15:03:14. Can you please help me to append the other info?

zdim
  • 64,580
  • 5
  • 52
  • 81
Kamal Nayan
  • 1,890
  • 21
  • 34
  • [BSD::stat](https://metacpan.org/pod/BSD::stat) looks promising for getting hires file times. – Shawn May 16 '19 at 07:08
  • @Shawn: Is it possible with internal perl modules? – Kamal Nayan May 16 '19 at 07:10
  • I don't think so. `stat` only returns seconds, and `POSIX::stat` is just it by another name. – Shawn May 16 '19 at 07:12
  • Once you get the time in nanoseconds... divide by 1000000000 to get seconds, use that to get the time as a human-readable string like you're doing, and use the remainder to get the leftover nanoseconds. – Shawn May 16 '19 at 07:18

2 Answers2

3

I must first say, that's not going to be accurate. Even as your hardware likely supports nanosecond resolution by the time it gets back to software it's hundreds of times off. (With a file's timestamp, presumably used only as relative to other files, that may be less important.)

Having said that, the only way I find to claim nanoseconds is to use system's tools, so stat

my ($ts) = grep { /^\s*Access: [0-9]{4}/ } qx(stat $file);
$ts =~ s/^\s*Access:\s+//;

or, using map also as a filter

my ($ts) = map { /^\s*Access:\s+([0-9]{4}.*)/ ? $1 : () } qx(stat $file);

One problem here, of course, is that we have to parse output; so see your man pages and test your stat, and don't hope for much portability. Watch for system changes as output format of programs may change. But I don't find nanoseconds claim in Perl.

For reference, on my CentOS 7 (under both bash and tcsh)

perl -we'$f=shift; print grep { /^\s*Access: [0-9]{4}/ } qx(stat $f)' file

prints, for a random file

Access: 2019-05-15 13:21:57.723987422 -0700

Once again, the "nano"seconds aren't accurate at all.


Another option, for perhaps more reliable output parsing, is to use ls with its --full-time

my $ts = join ' ', (split ' ', qx(ls --full-time $file))[5..7];

As always, when running external commands build your variables with care and quote and escape appropriately. A good tool is String::ShellQuote. Or, better avoid the shell altogether unless it's specifically needed. Best do all this using a module for running external tools.

zdim
  • 64,580
  • 5
  • 52
  • 81
  • Kudos for `ls --full-time` – Kamal Nayan May 16 '19 at 09:43
  • [Time::HiRes's version of stat](https://perldoc.pl/Time::HiRes#stat) will return subseconds if available. If you do have to shell out, you should quote your filename with [String::ShellQuote](https://metacpan.org/pod/String::ShellQuote) or bypass shell parsing such as with [IPC::ReadpipeX](https://metacpan.org/pod/IPC::ReadpipeX). – Grinnz May 16 '19 at 15:01
  • @Grinnz Well, that doesn't help -- [`Time::HiRes::stat`] says "_In any case do not expect nanosecond resolution, or even a microsecond resolution._" As for running external commands, yeah, sure, I hope they do it right. I'll add a comment – zdim May 16 '19 at 15:32
  • @KamalNayan Welcome. I recall a version of man page for `ls` from years ago saying in bugs section "Too many options" or such :) It's always worth checking whether there's something – zdim May 18 '19 at 03:58
2

This is a multi-part challenge. The first challenge is to get a sub-second resolution on your file times, which is both OS and filesystem-dependent. The second part is to get the local timezone offset. All of this is doable on a modern system, however. You were on the right track with Time::HiRes, but strftime() is only going to get you part way. Here is an example program which takes files as arguments and prints the hi-res mtime per your requirements.

#!/usr/bin/perl

use strict;
use warnings;

use Time::HiRes qw(stat);
use Time::Piece;
use Time::Seconds;

sub timestr {
    my $t = localtime($_[0]);
    my $tstr = $t->strftime('%Y-%m-%d %H:%M:%S');
    if ($_[0] =~ /(\.\d+$)/) { $tstr .= $1 }
    my $off = $t->tzoffset;
    return sprintf('%s %+02d%02d', $tstr, int($off->hours), $off->minutes % 60);
}

for (@ARGV) {
    my @stat = stat($_);
    print "$_\n  ".($! ? "Error: $!" : timestr($stat[9]))."\n";
}
exit(0);

Example output:

$ ./stat.pl stat.pl
stat.pl
  2019-05-16 20:02:38.76708 +1000

A lot of the heavy lifting is being done by Time::Piece, but there is a little bit of hackery going on to extract the fractional part of the time, and convert the timezone to the desired format (and I haven't tested it extensively). You may want to use sprintf() on the original incoming value to coerce a particular number of decimal places. My code just honestly reproduces the original fractional part.

Bear in mind that not all timestamps will have a fractional part, even on a filesystem which supports sub-second resolution. The file may have been copied from a source which does not have sub-second resolution, for example.

TFBW
  • 989
  • 7
  • 12
  • In retrospect, I think it's best to start the timestr() function by formatting the incoming number with sprintf(), then splitting it on the decimal place. This keeps all the time arithmetic nicely integer-based, and ensures you have a set number of decimal places. – TFBW May 16 '19 at 10:25