Most robust way to list every basename in a directory, sorted by modification date?

In zsh,

list=(*(Nom:r))

Is definitely the most robust.

print -rC1 -- *(Nom:r)

to print them one per line, or

print -rNC1 -- *(Nom:r)

as NUL-delimited records to be able to do anything with that output since NUL is the only character not allowed in a file path.

Change to *(N-om:r) if you want the modification time to be considered after symlink resolution (mtime of the target instead of the symlink like with ls -Lt).

:r (for root name) is the history modifier (from csh) to remove the extension. Beware that it turns .bashrc into the empty string which would only be a concern here if you enabled the dotglob option.

Change to **/*(N-om:t:r) to do it recursively (:t for the tail (basename), that is, to remove the directory components).

Doing it reliably for arbitrary file names with ls is going to be very painful.

One approach could be to run ls -td -- ./* (assuming the list of file names fits in the arg list limit) and parse that output, relying on the fact that each file names starts with ./, and generate either a NUL-delimited list or a shell-quoted list to pass it to the shell, but doing that portably is also very painful unless you resort to perl or python.

But if you can rely on perl or python being there, you would be able to have them generate and sort the list of files and output it NUL-delimited (though possibly not that easily portably if you want to support sub-second precision).

ls -t | sed -e 's/.[^.]*$//'

Would not work properly for filenames that contain newline characters (IIRC some versions of macOS did ship with such filenames in /etc by default). It could also fail for file names that contain sequence of bytes not forming valid characters as . or [^.] could fail to match on them. It may not apply to macOS though, and could be fixed by setting the locale to C/POSIX for sed.

The . should be escaped (s/\.[^.]*$//) as it's the regexp operator that matches any character as otherwise, it turns dot-less files like foobar into empty strings.

Note that to print a string raw, it's:

print -r -- "$string"

print "$string" would fail for values of $string that start with -, even introducing a command injection vulnerability (try for instance with string='-va[$(uname>&2)1]', here using a harmless uname command). And would mangle values that contain \ characters.

Your:

find . -type f -print0 | while IFS= read -d '' -r l ; do print "${${l%.*}##*/}" ; done

Also has an issue in that you strip the .* before removing the directory components. So for instance a ./foo.d/bar would become foo instead of bar and ./foo would become the empty string.

About safe ways to process the find output in various shells, see Why is looping over find's output bad practice?


IMNSHO robustness and shell scripts are incompatible concepts (IFS is just a hack, sorry). I think there are only two ways to do what you want in a robust manner: either write a program in some sane language (Python, C, whatever) or use tools built specifically for robustness.

With csv-nix-tools (*) you can achieve this with:

csv-ls -c name,mtime_sec,mtime_nsec | 
csv-sort -c mtime_sec,mtime_nsec | 
csv-cut -c name |
csv-add-split -c name -e . -n base,ext -r | 
csv-cut -c base |
csv-header --remove

Rather self-explanatory.

If you want to just see the basenames of files, that would be enough, but usually, you want to do something useful with the data you just got. That's where sink tools are useful. Currently, there are 3: csv-exec (executes a command for each row), csv-show (formats data in human-readable form), and csv-plot (generates 2D or 3D graph using gnuplot).

There are still some rough edges here and there, but these tools are good enough to start playing with them.

(*) https://github.com/mslusarz/csv-nix-tools