12

Result of a normal find using find . ! -path "./build*" -name "*.txt":

./tool/001-sub.txt
./tool/000-main.txt
./zo/001-int.txt
./zo/id/002-and.txt
./as/002-mod.txt

and when sorted with sort -n:

./as/002-mod.txt
./tool/000-main.txt
./tool/001-sub.txt
./zo/001-int.txt
./zo/id/002-and.txt

however the desired output is:

./tool/000-main.txt
./zo/001-int.txt
./tool/001-sub.txt
./zo/id/002-and.txt
./as/002-mod.txt

which means output is sorted based on filename only, but folder information should be maintained as part of the output.

Edit: Make example more complicated as the subdirectory structure may include more than one level.

3
  • 2
    See this question I asked on SO: stackoverflow.com/questions/3222810/…
    – camh
    May 23, 2011 at 2:23
  • @camh - if possible I would like to use only unix commands. In any case my question is pretty much a duplicate of yours. Can you transfer the best solution to this thread (keep a link to the original anyways) so I can mark is as the solution?
    – unode
    May 23, 2011 at 8:51
  • If @Shawn makes the changes I suggested in my comment (use -printf instead of awk), I think that is the best solution. I've reworked my original implementation to use this method.
    – camh
    May 23, 2011 at 10:43

3 Answers 3

11

You need to sort by the last field (considering / as a field separator). Unfortunately, I can't think of a tool that can do this when the number of fields varies (if only sort -k could take negative values).

To get around this, you'll have to do a decorate-sort-undecorate. That is, take the filename and put it at the beginning followed by a field separator, then do a sort, then remove the first column and field separator.

find . ! -path "./build*" -name "*.txt" |\
    awk -vFS=/ -vOFS=/ '{ print $NF,$0 }' |\
    sort -n -t / |\
    cut -f2- -d/

That awk command says the field separator FS is set to /; this affects the way it reads fields. The output field separator OFS is also set to /; this affects the way it prints records. The next statement says print the last column (NF is the number of fields in the record, so it also happens to be the index of the last field) as well as the whole record ($0 is the whole record); it will print them with the OFS between them. Then the list is sorted, treating / as the field separator - since we have the filename first in the record, it will sort by that. Then the cut prints only fields 2 through the end, again treating / as the field separator.

3
  • 3
    Since this is with find(1), you can skip the awk part and use -printf '%f/%p\n'
    – camh
    May 23, 2011 at 3:20
  • indeed our setup is slightly more complicated. It does include variable subdir depths. Edited the question to reflect this fact. My apologies for not including this at first.
    – unode
    May 23, 2011 at 8:44
  • 1
    @Unode: Shawn's solution handles variable depth just fine, it's the canonical solution to this problem (up to minor variations). May 23, 2011 at 9:40
6

I would use files '-printf' to output name and path, sort by name, and cutoff the name in a last step. '###' is just a marker, to help cutting.

find -name "*.txt" -printf "%f###%p\n" | sort -n | sed 's/.*###//'

%f prints the filename, %p the whole path.

I simplified the find-command to get it into one line, of course you would leave the ! -path "./build*" part.

3

In zsh ≥4.3.10:

print -l -- **/*.txt~build*(oe\''REPLY=${REPLY:t}'\')
  • **/*.txt matches *.txt in the current directory and its subdirectories recursively.
  • ~build* excludes matches whose text begins with build* (like ! -path './build*'). (You need setopt extended_glob first.)
  • (oe\''…'\') is a sorting glob qualifier. REPLY=… constructs the string to sort from the string to return.
  • ${REPLY:t} is the basename (“tail”) of the path.
1
  • Lots of concatenated magic. Interesting but we are limited to sh syntax. +1
    – unode
    May 23, 2011 at 8:37

You must log in to answer this question.

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