3

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.'
}
4
  • 1
    Just noting that you already have an array of strings, not of filenames. Perhaps you'd like to load the array in order in the first place?
    – Jeff Schaller
    Apr 7, 2020 at 17:01
  • @JeffSchaller The array is loaded from recent files of vim and emacs, so I don’t think we can do better?
    – HappyFace
    Apr 7, 2020 at 19:41
  • Can you show how exactly it's loaded?
    – Jeff Schaller
    Apr 7, 2020 at 19:45
  • @JeffSchaller Here :D
    – HappyFace
    Apr 7, 2020 at 19:55

3 Answers 3

7

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})
1
  • zsh's stat builtin (which itself predates both GNU and BSD stat) has been supporting nanosecond precision since zsh version 5.6 Apr 9, 2020 at 7:10
3

You can use an approach similar to that one:

zmodload zsh/stat

array=(file1 file2...)

# store mtimes in an associative array:
typeset -A mtime
stat -nLF %s.%N -A mtime +mtime -- $array

# sort using the Oe glob qualifier
sorted_array=(/(e['reply=($array)']nOe['REPLY=$mtime[$REPLY]'])

(%N for nanoseconds requires zsh 5.6 or newer).

2

As Jeff pointed out, it will be easier to create your array sorted, as in:

set -A files $(ls -trd -- ~/a ~/b ~/c)

Here splitting the output of ls on $IFS character (by default containing SPC, TAB, LF and NUL). To split on LF only (to be able to work with filenames than contain SPC or TAB characters (but not LF obviously)), you can set IFS=$'\n' or use the f parameter expansion flag:

files=(${(f)"$(ls -trd -- $files)"})

(here also using zsh-style array=(...) syntax instead of ksh88-style set -A array ... since that syntax is already zsh-specific anyway).

0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .