'Sort list of files in different directories with zsh

Assume I have the following directory/file structure

dirA/1fileAA.zsh
dirA/99fileAB.zsh
dirB/2fileBA.zsh
dirB/50fileBB.zsh
dirB/subdirA/20fileBAA.zsh

which I want to have ordered by the numbers the filenames begin with, ignoring any directories, so I get

dirA/1fileAA.zsh
dirB/2fileBA.zsh
dirB/subdirA/20fileBAA.zsh
dirA/99fileAB.zsh
dirB/50fileBB.zsh

using just built-in zsh functionality.

What would be the best way to achieve this?

I could think of rewriting strings sort and write them back? Or better try to create an associated array and sort by keys?

I'm still a zsh and want to avoid digging into the wrong direction, too much.

zsh


Solution 1:[1]

Here is one way to accomplish this using only zsh builtins. The function prepends the filename to the front of each path for sorting and then removes it:

function sortByFilename {
    local -a ary
    printf -v ary '%s/%s' ${${argv:t}:^argv}
    print -l ${${(n)ary}#*/}
}

With your example directory setup, it can be invoked from the parent directory of dirA and dirB with:

sortByFilename **/*.zsh

Testing it:

sortByFilename \
    dirA/1fileAA.zsh \
    dirA/99fileAB.zsh \
    dirB/2fileBA.zsh \
    dirB/50fileBB.zsh \
    '/leadslash/42 and spaces' \
    dirB/subdirA/20fileBAA.zsh

Result:

dirA/1fileAA.zsh
dirB/2fileBA.zsh
dirB/subdirA/20fileBAA.zsh
/leadslash/42 and spaces
dirB/50fileBB.zsh
dirA/99fileAB.zsh

The pieces:

  • printf -v ary <fmt> ...: runs printf with the format string, and assign the results to the ary array. Each iteration of the format string will become another element in the array.
    • %s/%s: the format string. This will concatenate two strings with a slash separator.
      If there are more values than in the input than specifiers in the format string, printf will repeat the format pattern. So here, it will pull pairs (of filename/pathname) from the input array.
    • ${${argv:t}:^argv}: this will produce an array alternating with filenames and full paths, i.e. (file1 path1 file2 path2 ...)
      • ${ :^ }: zsh parameter expansion that will zip two arrays to create the alternating filenames and paths.
      • ${argv:t}: array of filenames. Built using the function positional parameters in argv, and the :t modifier, which returns the filename component for each element in the array.
      • argv: array of full paths.
  • print -l: print each element of the input on a separate line.
    • ${${(n)ary}#*/}: the final sorted list of paths.
      • ${(n)ary}: Returns the array sorted numerically, using the n parameter expansion flag. At this point, each element in ary is the concatenation of the filename, a slash, and the input path.
        The n flag works here because of the filename pattern; it will sort by decimal value instead of lexically within a common / empty prefix, e.g. foo1 foo3 foo12.
      • ${ #*/}: Removes the pattern */ from the front of each element in the array. This deletes the prefix that was being used for sorting, leaving the original path.
  • local -a ary: declares an array variable. This is used as an indicator to printf -v to split its output.
    It's possible to eliminate this line and make the function shorter and a bit more cryptic by (re-/mis-/ab)using the pre-declared array argv.
    function sortByFilename {
        printf -v argv %s/%s ${${argv:t}:^argv}
        print -l ${${(n)argv}#*/}
    }
    

Edit - a single-line version:

(){print -l ${"${(n0)$(printf '%s/%s\0' ${${argv:t}:^argv})}"#*/}} **/*.zsh

Including this simply because one-liners are fun to create, not because it's recommended. With the anonymous function, process substitution, and additional parameter expansion flags, this is less readable and possibly less efficient than the function above.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1