0

In a directory, I have a number of files, and I need to do something with the most recently modified file with a .txt suffix.

Ideally, I'd like to do

myutility "$newest"

in the end, where $newest would be the pathname of the most recently modified file.

It would be nice if this additionally could be generalised so that I could get the most recently modified file with a .txt suffix in a directory hierarchy, and even better if I could get the, say, five most recently modified files so that I could use

myutility "${newest[@]}"

to run my utility on the five most recently modified .txt files in a whole directory hierarchy.

A solution using bash or ksh93 would be best.

1

Although you tagged the question bash and ksh, with zsh and glob qualifiers:

print **/*.txt(.om[1,5])

prints the first 5 plain files (.) with .txt extension, ordered by ascending modification time (om)

ex.

print **/*.txt(.om[1,5])
dir/file1.txt file2.txt dir/file.txt File1.txt File2.txt
  • I should really read up on the zsh shell's operators etc. someday soon... How would you go about getting the results into an array? I realise that just using the expansion on the command line of a utility would work, but just for completeness... Same syntax as bash and ksh93? – Kusalananda Jul 18 '18 at 12:54
  • @Kusalananda yes I believe you can do a ksh/bash style newfiles=( **/*.txt(.om[1,5]) ) in zsh (although that may not be the recommended way - I'm just feeling my way into zsh myself) – steeldriver Jul 18 '18 at 12:59
1

The bash shell (and a number of other shells) has a non-standard -nt test operator. This operator tests whether the modification time on one file is newer than the other.

In bash, the -nt operator has a time resolution of one second. In ksh93 one may use the same operator with sub-second resolution. The code below is compatible with ksh93, except that shopt -s globstar need to change to set -o globstar where this option is used.


Using the -nt test operator on the files that match *.txt in the current directory to find the newest regular file:

unset newest
for filename in ./*.txt; do
    if [ -f "$filename" ] && [ "$filename" -nt "$newest" ]; then
        newest=$filename
    fi
done

or, slightly shorter,

unset newest
for filename in ./*.txt; do
    [ -f "$filename" ] && [ "$filename" -nt "$newest" ] && newest=$filename
done

At the end of the loop, the value "$newest" would be the the filename of the most recently modified regular file in the current directory that has a filename suffix .txt, provided that there were any such files at all (otherwise, the newest variable would be unset).

The dash shell, if this is used, would need a separate test for the existence of the $newest file as its -nt operator has slightly different semantics from both bash and ksh93:

unset newest
for filename in ./*.txt; do
    if [ -f "$filename" ]; then
        if [ ! -f "$newest" ] || [ "$filename" -nt "$newest" ]; then
            newest=$filename
        fi
    fi
done

To do this recursively with bash, set the globstar shell option and use ** to do the filename globbing:

shopt -s globstar
unset newest
for pathname in ./**/*.txt; do
    if [ -f "$pathname" ] && [ "$pathname" -nt "$newest" ]; then
        newest=$pathname
    fi
done

The ** pattern matches just like * but allows matching across / in pathnames.

The dash shell does not support this.


Building on the previous code, to get the five most recently modified files:

shopt -s globstar
unset newest
for pathname in ./**/*.txt; do
    if [ -f "$pathname" ]; then
        for (( i=0; i < 5; ++i )); do
            if [ "$pathname" -nt "${newest[$i]}" ]; then
                newest=( "${newest[@]:0:i}" "$pathname" "${newest[@]:i:4}" )
                break
            fi
        done
    fi
done

The only thing extra here is that newest now is a bash array, and that we, for each pathname that we consider, have to go through the five most recently found "newest files" to see where in the array this new file may fit in.

When we have found that the file is indeed newer than one of the other files in the array, we splice the new pathname into the array and at the same time limit the number of elements in it to five.

After the end of the loop, the newest array will hold the pathnames of the five most recently modified regular files having a .txt filenames suffix. The array will be sorted on modification time, with the most recently modified file as ${newest[0]}.

  • It could help to mention shells that support it ;-) – schily Jul 18 '18 at 10:50
  • @schily I will add more a tiny bit more later (no time right this moment). – Kusalananda Jul 18 '18 at 10:53
  • 1
    Note that neither dash nor mksh support nanoseconds. You need to use bosh, ksh93, or zsh`. – schily Jul 18 '18 at 11:00
  • @schily I added a note about ksh93 at the top. – Kusalananda Jul 18 '18 at 11:55
  • Why not the other shells? – schily Jul 18 '18 at 12:00
0

You can list the files by modification time (or creation or access) with ls.

simply say

ls -t *.txt | head -n5 

to get the last 5 recent .txt files

You can feed it into myutility with $() or xargs.

ls -t *.txt | head -n5 | xargs myutility

If you need the recursion, you can use find.

find -type f -iname "*.txt" | xargs ls -t | head -n5 | xargs myutility

(possible issue: you have to have at least one .txt file for this to work. you can fix it, but it's the cleanest solution).

EDIT: OP wanted to know how to handle filenames with spaces. Here is one solution:

find -type f -iname "*.txt" | xargs ls -t | head -n5 | tr '\n' '\0' | xargs -0 myutility

If you want to solve the possible issue mentioned above you can check for the files before-head with find ..... | egrep '.*' || exit or simply include some nonsensical filename to ls and ignore the warning like xargs ls -t ''.

  • This requires that the filenames do not contain whitespace characters such as space or newline. It also requires GNU find. – Kusalananda Jul 18 '18 at 12:07
  • of course. I wrote it this way for simplicity. You can easily fix it by using zero delimiter. option -0 for xargs and print0 for find. – goteguru Jul 18 '18 at 12:09
  • Good people never use whitespace in filenames anyway... :-D – goteguru Jul 18 '18 at 12:09
  • I don't think I mentioned anything about the character set used for the filenames in the question, apart from them ending with .txt. So there can be no assumptions made. Good people write code to support any valid Unix filename. Using -print0 and -0 with find and xargs will help partway through the pipeline (if you have tools that support these options), but you'll have the same issue when you get to ls and head later, especially if the filenames contain newlines. – Kusalananda Jul 18 '18 at 12:16
  • no insult was intended. I hoped you are here to learn, and simple is better than complex if you want to learn. Extended solution included for you. – goteguru Jul 18 '18 at 12:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.