1

How can I find files that contain specific content (grepable) and sort the found files by last modified time? (I want to select just the one newest file)

This command finds files:

find subfolder/ -maxdepth 1 -type f -exec grep -l 'blue_wizards' {} \;`

This command finds files and sort them:

find subfolder/ -maxdepth 1 -type f -printf "%T+ %p\n" | sort

But if I supply -printf the input to -exec grep will be not what I want.

2
  • Do you only want to see a list of matching files sorted by modification time or do you also want to see the last modification time of each file? Jul 26 at 15:47
  • I just want to select 1 file, the one latest modified. So modification time is not interesting to see, it's just the sorting condition.
    – Moberg
    Jul 27 at 6:20
2

It would be easier to use zsh and its glob qualifiers:

grep -le blue_wizards -- subfolder/*(D.Om)

Where:

  • D also includes hidden files like find does by default
  • . restricts to regular files (like -type f)
  • Om sorts in reverse by age like ls -rt with oldest file first

You can do the same with GNU find, cut and xargs with any Bourne-like shell with:

find subfolder/ -maxdepth 1 -type f -printf '%T@\t%p\0' |
  LC_ALL=C sort -zn |
  cut -zf2- |
  xargs -r0 grep -le blue_wizards --

With older versions of GNU cut, you may need to use tr '\0\n' '\n\0' | cut -f2- | tr '\0\n' '\n\0' instead or resort to GNU sed which, like GNU sort has been supporting -z for much longer: LC_ALL=C sed -z 's/^[^:]*://' (and change the \t to : in the -printf argument).

Note that in any case. -printf is GNU-specific (-maxdepth is also a GNU extension, though now supported by more find implementations).

%T@ is better than %T+ as it avoids computing the calendar date for the corresponding mtime, but also more importantly because it works correctly around DST changes. If using %T+ for sorting, you'd want to fix TZ to UTC0 for instance to get UTC timestamps not affected by DST.

Compare:

$ ls --full-time -ldrt a b
-rw-r--r-- 1 stephane stephane 13 2020-10-25 01:43:28.989189400 +0100 b
-rw-r--r-- 1 stephane stephane 13 2020-10-25 01:31:20.112312300 +0000 a
$ find a b -printf '%T+ %T@ %p\n' | sort
2020-10-25+01:31:20.1123123000 1603589480.1123123000 a
2020-10-25+01:43:28.9891894000 1603586608.9891894000 b
$ TZ=UTC0 find a b -printf '%T+ %T@ %p\n' | sort
2020-10-25+00:43:28.9891894000 1603586608.9891894000 b
2020-10-25+01:31:20.1123123000 1603589480.1123123000 a

a is newer than b even though it appears older with %T+ as it was modified after the switch to winter time in my timezone (Europe/London here).


To get the newest file, you'd likely want to reverse that order (change Om to om and sort to sort -r) and get grep to output the list of files NUL-delimited instead of newline-delimited so it can reliably be post-processed (as newline is as valid a character as any in a file name), for which you'll need GNU grep or compatible and get the first one.

For instance to store the path of the newest file in the $newest variable (and return with a failure exit status if there's no matching file):

zsh + GNU grep:

grep --null -le blue_wizards -- subfolder/*(D.om) |
  IFS= read -rd '' newest

bash + GNU find + GNU sort + GNU cut + GNU xargs + GNU grep:

IFS= read -rd '' newest < <(
  find subfolder/ -maxdepth 1 -type f -printf '%T@\t%p\0' |
    LC_ALL=C sort -rzn |
    cut -zf2- |
    xargs -r0 grep --null -le blue_wizards --
)

to get all of them (newest first) into an array:

zsh + GNU grep:

files=(${(0)"$(grep --null -le blue_wizards -- subfolder/*(D.om))"})
print -r newest: $files[1]

bash 4.4+ + GNU find + GNU sort + GNU cut + GNU xargs + GNU grep:

readarray -td '' files < <(
  find subfolder/ -maxdepth 1 -type f -printf '%T@\t%p\0' |
    LC_ALL=C sort -rzn |
    cut -zf2- |
    xargs -r0 grep --null -le blue_wizards --
)
printf '%s\n' "newest: ${files[0]}"

If you wanted to do that POSIXly, that is only using standard sh and utility syntax (so without all the GNU or zsh extensions), it would be impossible to do it without making some assumptions on what byte values the file names may contain. If you can guarantee the file names don't contain newline characters and are reasonably short, you should be able to do:

dir=subfolder
file=$(
  export LC_ALL=C
  CDPATH= cd -P -- "$dir" &&
    ls -At |
      sed 's/"/"\\""/g; s/.*/"&"/; s|^|./|' |
      xargs -E '' sh -c '
        [ "$#" -eq 0 ] || 
          find "$@" -type f -exec grep -l blue_wizards {} +' sh |
      head -n 1
)
[ -n "$file" ] && newest="$dir/$file"

If you can make even more assumptions like the file names also only contain alnums, ., - characters from the portable character set, don't start with -, are all regular files, $CDPATH is not set, $dir doesn't contain .. components, doesn't start with - nor +:

dir=subfolder
file=$(
  cd "$dir" &&
    ls -At |
      xargs grep -l blue_wizards |
      head -n 1
)
[ -n "$file" ] && newest="$dir/$file"
0
2

Simplest may be to combine both find commands like so:

find subfolder/ -maxdepth 1 -type f \
    -exec grep -q 'blue_wizards' {} \; \
    -printf "%T+ %p\n" | sort

The thing here is that -exec ; can be used as a condition, it's truthy or falsy based on the exit status of the command. Also grep -q to just get the exit status, and no output.

That requires spawning a copy of grep for each file, so it's not exactly optimal in that. -exec {} + doesn't work the same way: you couldn't get exit statuses for the individual files that way, so it always evaluates as truthy. Also, with -printf "%T+ %p\n" you'll have issues if any filenames contain newlines.

5
  • 1
    See also the issue with using %T+ in my answer. Jul 26 at 15:58
  • Thanks, I tried to combine them, but I must have been doing something wrong. I went with find module_root/ -maxdepth 1 -type f -exec grep -q '#%Module' {} \; -printf "%T@ %p\n" | sort -r | head -n1 | cut -d' ' -f2
    – Moberg
    Jul 27 at 12:10
  • @Moberg, mm. I don't have your exact files of course, but I can't see immediately what would be wrong with that. It works for some test files I just made. Run it in smaller pieces, see if the find produces the right output, or even sees the right files etc.
    – ilkkachu
    Jul 27 at 12:16
  • AH sorry, it works great, thanks. What I mean was that my initial try that made me write the question didn't work. Now it works! :)
    – Moberg
    Jul 27 at 13:31
  • @Moberg, ah, ok!
    – ilkkachu
    Jul 27 at 13:31

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.