Tuesday, June 14, 2011

Epidose #149: Tiiiiime is on my file...

Tim checks the clock

This week we had a tough time coming up with episode ideas. We combed the internet looking for ideas and we finally came across this idea. And wouldn't you know, it just so happend that I actually needed to use this bit of fu this week.

What I wanted to know is how long how many seconds (or minutes) had passed since any file in a given directory had been modified. Unfortunately, the cmd.exe version of this command is big ol script and looks something like this, so we'll have to stick with the PowerShell version.

First, let's find the most recently modified file:

PS C:\> ls | ? { !$_.PSIsContainer } | sort LastWriteTime -desc | select -f 1

Directory: C:\

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 4/7/2011 1:43 PM 2880 myfile.txt

We start off by getting a directory listing by using Get-ChildItem. The results are piped into the Where-Object cmdlet to filter for objects that aren't containers, which leaves files. We then sort the objects by the LastWriteTime in descending order. Finally, we select the first object.

To get the date difference all we need to do is take LastWriteTime property of our object and subtract it from the current time. The verbose version of the command looks like this:

PS C:\> (Get-Date) - (Get-ChildItem | Where-Object { -not $_.PSIsContainer } |
Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1).LastWriteTime

Days : 66
Hours : 10
Minutes : 26
Seconds : 34
Milliseconds : 623
Ticks : 57399946235000
TotalDays : 66.4351229571759
TotalHours : 1594.44295097222
TotalMinutes : 95666.5770583333
TotalSeconds : 5739994.6235
TotalMilliseconds : 5739994623.5

This is very similar to our last command, except for the subtraction and the output of the difference.

The command is a bit long, but we can shorten it using aliases, positional parameters, and shortened parameter names.

PS C:\> (Get-Date) - (ls | ? { !$_.PSIsContainer } | sort LastWriteTime -desc | select -f 1).LastWriteTime

If we just want the total number of seconds, we can get that too.

PS C:\> ((Get-Date) - (ls | ? { !$_.PSIsContainer } | sort LastWriteTime -desc | select -f 1).LastWriteTime).TotalSeconds


So we have the most recent time, but what if we want the file name too? We can get the name of the file using a slightly different approach by adding a new property to the object:

PS C:\> (ls | ? { !$_.PSIsContainer } | sort LastWriteTime -desc | select -f 1) |
select name, @{Name='SecondsSinceMod';Expression={((Get-Date) - $_.LastWriteTime).TotalSeconds}}

Name SecondsSinceMod
---- ---------------
myfile.txt 5740999.9526015

The Select-Object cmdlet is used to select the Name and create a new property, SecondsSinceMod. A hashtable is used to create these properties, and we need to provide the name and an expression (value). The name is SecondsSinceMod and the Expression is what we did earlier.

This episode is quick, and easy. Oh, time, time, time is on my side...

Hal checks out

Well, personally, I can't get no satisfaction this week. If I restrict myself to the rules we set ourselves for the blog, there doesn't actually seem to be a general solution for this problem that works across all Unix platforms without resorting to a higher-level scripting language like Perl.

We've already covered an idiom for figuring out the most recently modified file in a directory, namely "ls -t | head -1". But the problem is getting ls to report out the timestamp on the file in seconds. The GNU version of ls actually has a (very non-standard) "--time-style" option for specifying a time output format. We can leverage this to solve the problem:

$ echo $(( $(date +%s) - $(ls -lt --time-style=+%s | awk 'NR == 2 {print $6}') ))

To understand what's going on here, it helps to break things down into pieces. First we grab the output of "date +%s", which gives us the current time in Unix "epoch time" format (seconds since Jan 1, 1970). Then we use "ls -lt ..." to output the contents of the current directory sorted by last modified time, using "--time-style" to output the timestamp in epoch time format. This gets piped into awk, which prints out the time stamp field from the second line of output (the first line being a header from "ls -l"). All of that is wrapped up in a "echo $(( ... - ... )) expression which prints out the difference between the current time and the timestamp on the most recently modified file-- i.e., the number of seconds since that file was changed.

Another way of getting the mtime for a file in epoch time format is with the stat command-- at least if you're on Linux or BSD (Solaris doesn't seem to have implemented a stat command, for example). However, the option for specifying epoch time format is different depending whether you're using the Linux or BSD version of the command. The Linux version is "stat -c %Y <filename>", while BSD is "stat -f %m <filename>".

So a BSD solution for our challenge would be:

$ echo $(( $(date +%s) - $(stat -f %m $(ls -t | head -1)) ))

In this case we use our "ls -t | head -1" idiom to get the file name of the most recently modified file. That file name then becomes the argument to our stat command, which gives us the epoch time stamp for that file. We then use this value to compute the difference from the current time, just as we did in the last example.

In summary then: this challenge is solvable on Linux or any system that happens to have the GNU ls command installed. And for BSD, there's an option using the stat command. That's pretty decent coverage, but not a completely portable solution. And that's really sort of annoying.

I guess Tim gets one of his rare "wins" this week. Well played, sir. Well played!