4

So I know parsing ls is bad!.

Today I found this nugget:

FILENAME=`ls -t $READ_FOLDER | head -1`

So I believe originally this was designed to grab the newest filename, I actually now want to adapt it, to grab maybe the 5th newest file as the process that's checking it is a little slow, and so we can't be sure they have been processed yet.

I'm working in bash, but not opposed to a sh generic solution too.

5 Answers 5

8

Perlishly:

#!/usr/bin/env perl

use strict;
use warnings;

my %files_by_mtime;

foreach my $file ( glob ( "./*" ) ) {
   my ( $mtime ) = (stat($file))[9];
   $files_by_mtime{$file} = $mtime; 
}

foreach my $f (  sort { $files_by_mtime{$b} <=> $files_by_mtime{$a} } keys %files_by_mtime ) {
   print "$f $files_by_mtime{$f}\n";
}

Reducing to:

perl -e '%f = map { $_ => (stat)[9] } glob("./*");print "" . ( sort { $f{$b} <=> $f{$a} } keys %f )[4];'

Where the "N"th file is 4, because perl arrays start from zero.

6

Assuming you have access to GNU tools (you do if you're running Linux), I would use stat instead. For example:

$ ls -l
total 0
-rw-r--r-- 1 terdon terdon 0 Sep 15 16:49 file1
-rw-r--r-- 1 terdon terdon 0 Sep 15 16:39 file2
-rw-r--r-- 1 terdon terdon 0 Sep 15 16:29 file3
-rw-r--r-- 1 terdon terdon 0 Sep 15 16:19 file4
-rw-r--r-- 1 terdon terdon 0 Sep 15 16:09 file5
-rw-r--r-- 1 terdon terdon 0 Sep 15 15:59 file6
-rw-r--r-- 1 terdon terdon 0 Sep 15 15:49 file7

So, the 5th newest file is file5. To print just that, you can do:

$ stat --printf '%Y %n\0' * | sort -zrnk1 | 
    awk -vRS='\0' 'NR==5{sub(/^[^ ]* /,"",$0); print}'

You could then easily make this into a shell function that can take N (5 in your example) as an argument. Just add these lines to your ~/.bashrc or equivalent:

nthfile() {
  stat --printf '%Y %n\0' * | sort -zrnk1 |
     awk -vRS='\0' -vn="$1" 'NR==n{sub(/^[^ ]* /,"",$0); print}'
}

Note that this will also show you directories. If you need it to match hidden files as well run (assuming you're using bash) shopt -s dotglob before the above command.

Explanation

  • stat --printf '%Y %n\0' * : for each file or directory in the current folder, print the modification date in seconds since the epoch (%s) and the file name (%n) and end each line with \0 instead of \n. This lets us deal correctly with file names containing newline characters.
  • sort -zrnk1 : sort the output in reverse sort order (-r), from the newest to the eldest. The -z tells sort to expect null-terminated input lines. The -n tells it to sort numerically and the -k1 to only consider the first field when sorting.
  • awk:
    • -vRS='\0' : set the input record (line) separator to \0;
    • vn=$1 : set the variable n to whatever was given as input to the function;
    • NR==n{} : run this only on line n (5 in the first example);
    • sub(/^[^ ]* /,"",$0); print : substitute all non-space characters from the beginning of the line (^[^ ]*) up to the 1st space and print the result
7
  • 1
    Don't you need to tell sort to sort numerically (sort -zrnk1 or something like that)?
    – zwol
    Sep 15, 2015 at 18:34
  • Shell pattern * does not match filenames starting with ..
    – Roman
    Sep 15, 2015 at 21:23
  • @zwol it shouldn't be necessary, no. The output of stat will always be seconds since the epoch so sort should always deal with it correctly.
    – terdon
    Sep 15, 2015 at 21:40
  • @Roman true but neither does ls -t which is what the OP was using. There was no mention of dotfiles in the question. Still, I edited to add the dotglob option for bash users.
    – terdon
    Sep 15, 2015 at 21:40
  • 1
    Thanks for dotglob. About sort without -n: I believe, it will correctly sort numbers, only if they all have the same length (same amount of digits).
    – Roman
    Sep 15, 2015 at 21:48
3

You can use GNU find to list the files with the modified time expressed as epoch time, then use sort to sort the list, finally head and tail to get the desired numbered file name :

find . -maxdepth 1 -type f -printf '%T@ %p\n' | sort -k1,1nr | head -5 | tail -1
1
  • 5
    Workaround for unusual filenames: find . -maxdepth 1 -type f -printf '%T@ %p\0' | sort -z -k1,1nr | awk -v 'RS=\0' 'NR==5'
    – Roman
    Sep 15, 2015 at 17:04
3

With zsh:

print -r -- *(.Dom[5])

give you the 5th file in current directory sorted by modification time. Change m to a to sort by access time. . and D include only regular and hidden files.

2

One way to do this (assuming no newlines in your file names) would be

ls -t ... | head -n5 | tail -n1
5
  • @don_crissti: this is not parsing (the output) of ls, it's just using it. Sep 15, 2015 at 14:31
  • @don_crissti I overlooked the link given by the OP. The problem is not ls, but the processing in the pipe which will break at filenames with embedded newlines etc. Sep 15, 2015 at 14:41
  • 1
    @ThomasErker this is reading the output of ls and modifying its output depending on certain criteria. Otherwise known as parsing it. Like most cases of parsing ls output, it will fail on file names with newlines.
    – terdon
    Sep 15, 2015 at 14:54
  • As @terdon has pointed out, part of the reason for avoiding ls is that I can't be sure of the file names. Generally they're consistent, but considering the code I quoted, I don't trust them. Sep 15, 2015 at 15:05
  • Filenames may not contain "/" or NUL (0x00). Parsing ls always leads to pe
    – waltinator
    Sep 15, 2015 at 22:16

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .