Zsh – How to Sort Array by Modification Date

arraydatefilessortzsh

How to sort a zsh array by modification date?

files=( ~/a ~/b ~/c )
# how to sort files by date?

PS: Here is my exact usecase, (fz is almost fzf)

v () {
 local files
 files=() 
 command rg '^>' ~/.viminfo | cut -c3- | while read line
 do
  [ -f "${line/\~/$HOME}" ] && files+="$line" 
 done
 test -f ~/.emacs.d/.cache/recentf && {
  command rg --only-matching --replace '$1' '^\s*"(.*)"$' ~/.emacs.d/.cache/recentf | while read line
  do
   [ -f "$line" ] && files+="$line" 
  done
 }
 files="$(<<<"${(F)files}" fz --print0 --query "$*")"  || return 1
 files="${files//\~/$HOME}" 
 local ve="$ve" 
 test -z "$ve" && ! isSSH && ve=nvim 
 "${ve:-vim}" -p ${(0@)files}
 : '-o opens in split view, -p in tabs. Use gt, gT, <num>gt to navigate tabs.'
}

Best Answer

It's a lot easier if you sort the list when you build it. But if you can't…

A classic approach is to add the sort criterion to the data, then sort it, and then remove the added cruft. Build an array containing timestamps and file names, in a way that is unambiguous and with the timestamps in a format that can be sorted lexicographically. Sort the array (using the o parameter expansion flag), then strip the prefix. You can use the stat module to obtain the file's modification time.

zmodload zsh/stat
for ((i=1; i<$#files; i++)); do times[$i]=$(stat -g -F %020s%N +mtime -L -- $files[$i]):$files[$i]; done
sorted=(${${(o)times}#*:})

The %N format to zstat (to get timestamps at nanosecond resolution) requires zsh ≥5.6. If your zsh is older, remove it and the code will still work, but comparing timestamps at a 1-second resolution. Many filesystem have subsecond resolution, but I don't think you can get it with the zsh stat module in older versions of zsh.

If your zsh is too old, you can get more precise timestamps with the stat utility from GNU coreutils. If you have it, you probably have the other GNU coreutils as well, so I'll use those. GNU coreutils are typically present on non-embedded Linux, but might not be on BSD or macOS. On macOS, you can install them using brew. If the GNU coreutils aren't part of the base operating system, you may need to change stat to gstat, sort to gsort and cut to gcut.

if (($#files)); then
  sorted=(${(0@)"$(stat  --printf='%040.18Y:%n\0' "$files[@]" | sort -rz | cut -z -d':' -f2-)"})
else
  sorted=()
fi

An alternative zsh approach is to build a pattern that includes all the files in $files and more. Sort the files matching this pattern, then filter it to include only the desired files. You do need to build the whole pattern for more_files, which may not always be practical.

more_files=(~/*(Om))
sorted=(${more_files:*files})