Why does find return different, sorted results when passed the current directory versus a path?

Viewed 96

I'm attempting to write a shell command returning the most recently modified file. However, the directory has more files than ls can accommodate directly (hence using find, which I understand to be the proper solution for piping into other commands anyway).

In doing so, I've encountered a confusing difference between running find . in the directory, and passing the directory to find (i.e. find $folder).

Here is an example:

➜  echo $SHELL
/usr/local/bin/zsh
➜  pwd
/Users/aresnick/Downloads
➜  find . -type f -maxdepth 1 -print0 | xargs -0 ls -t | head -1
./DID_Codebook_2020.pdf
➜  find $(pwd) -type f -maxdepth 1 -print0 | xargs -0 ls -t | head -1
/Users/aresnick/Downloads/Profiles17_MA.pdf
➜  find /Users/aresnick/Downloads -type f -maxdepth 1 -print0 | xargs -0 ls -t | head -1
/Users/aresnick/Downloads/Profiles17_MA.pdf

Note that DID_Codebook_2020.pdf is not the same file as Profiles17_MA.pdf. How can I explain why these commands seem to return different results?

For reference, here is the output of ls -halt | head -5 in the directory (including the two most recent files which are getting confused here).

➜ ls -halt | head -5
total 51229416
drwxr-xr-x+   92 aresnick  staff   2.9K Oct 12 10:34 ..
drwx------@ 4033 aresnick  staff   126K Oct 12 10:02 .
-rw-r--r--@    1 aresnick  staff   1.7M Oct 12 10:02 DID_Codebook_2020.pdf
-rw-r--r--@    1 aresnick  staff   470K Oct 12 10:00 Profiles17_MA.pdf

Looking more closely via stat, it appears that the change time is in fact newer on Profiles17_MA.pdf:

➜  stat -f "ACCESS-%Sa   CHANGE-%Sc %SN" {DID_Codebook_2020.pdf,Profiles17_MA.pdf}
ACCESS-Oct 12 10:02:14 2020   CHANGE-Oct 12 10:02:08 2020 DID_Codebook_2020.pdf
ACCESS-Oct 12 10:02:14 2020   CHANGE-Oct 12 10:02:10 2020 Profiles17_MA.pdf

However, I don't see how this explains why the sorting order would change with find . vs. find ~/Downloads.

What gives? I have the vague intuition that this may have something to do with the fact that we're looking at inodes' change times, and something about including the directory changes…something.

Thanks in advance! Note that while I am also interested in solutions to my specific sorting problem, I'm most interested in understanding why the difference occurs.

1 Answers

The job of xargs is to split up command lines which are longer than the system can accommodate. Let's assume for illustration that you have four files and an OS where ARG_MAX is just 25 bytes (though in real life it will be on the order of megabytes on modern systems). Then without a path, xargs runs

ls -t ./a ./b ./c ./d

but with full paths, the command line has to be split up into two invocations, so as not to exceed the limit on how long the command line can be:

ls -t /path/to/a /path/to/b
ls -t /path/to/c /path/to/d

Now of course if d is your newest file, head will still return the newest file from the first ls invocation.

On a system with GNU find (Linux etc) use its -printf option to format a string where the modification time precedes the file name.

find . -type f -printf "%T+\t%p\0" |
sort -rnz |
perl -n000 's/^[^\t]+\t//; print; exit'

We sort by modification time, then discard the modification time and print just the file name. The null delimiter takes care of handling even file names with newlines in them; this feature in particular is a GNU extension.

If you don't have the GNU utilities, you can use find -exec stat with a similar format string; unfortunately, this feature of stat is not standardized, either, but it should not be hard to find examples for non-Linux systems like BSD, macOS, etc.

Related