5

I want to sort

  1. based on the file name.
  2. For the case where the file name prefix matches and the file ends in a number, I would like to have those numerically sorted based on the number at the end of the file name.

The following

cat /tmp/foo.txt | sort -t/ -k3,3 -k3,3n

accomplishes 1, but not 2.

Input /tmp/foo.txt

dirA/catA/apple.txt
dirA/catA/addition.txt
dirA/catA/difference
dirA/catB/binary.txt
dirA/catB/carry.txt
dirA/catB/digit
dirA/catC/test-10.txt
dirA/catC/test-100.txt
dirA/catC/test-1000.txt
dirA/catC/test-11.txt
dirA/catC/test-2.txt
dirA/catC/test-20.txt
dirA/catC/test-25.txt
dirA/catC/test-5.txt
dirA/catC/test-50.txt
dirA/catC/test-500.txt
dirA/catC/test-7.txt
dirA/catC/test-75.txt
dirA/catC/test-8.txt
dirA/catC/abc-test-9.txt
dirA/catC/abc-test-999.txt
dirA/catC/abc-test-75.txt
dirA/catC/abc-test-8.txt

Desired Output

dirA/catC/abc-test-8.txt
dirA/catC/abc-test-9.txt
dirA/catC/abc-test-75.txt
dirA/catC/abc-test-999.txt
dirA/catA/addition.txt
dirA/catA/apple.txt
dirA/catB/binary.txt
dirA/catB/carry.txt
dirA/catA/difference
dirA/catB/digit
dirA/catC/test-2.txt
dirA/catC/test-5.txt
dirA/catC/test-7.txt
dirA/catC/test-8.txt
dirA/catC/test-10.txt
dirA/catC/test-11.txt
dirA/catC/test-20.txt
dirA/catC/test-25.txt
dirA/catC/test-50.txt
dirA/catC/test-75.txt
dirA/catC/test-100.txt
dirA/catC/test-500.txt
dirA/catC/test-1000.txt

3 Answers 3

7

Perl to the rescue!

perl -e '
    print for sort { (($a =~ m{.*/([^0-9]*)})[0] cmp ($b =~ m{.*/([^0-9]*)})[0])
                     ||
                     (($a =~ /-([0-9]+)/)[0] <=> ($b =~ /-([0-9]+)/)[0]) } <>
' -- /tmp/foo.txt
  • <> reads the lines of input
  • sort sorts the list based on the given code
  • m{.*/([^0-9]*)} extracts the basename up to a digit (if present)
  • cmp does the string comparison
  • If they are equal, the || "or" applies the second comparison, where:
  • /-([0-9]+)/ extracts the number
  • <=> does the numeric comparison
  • The (...)[0] construct is needed as matching returns a list of matches (corresponding to $1, $2, etc). List context is needed to get the matches. We're interested in the first match only (as there's no other).
6
awk '
    BEGIN {FS = "[-/.]"; OFS = "\t"}
    {n = 0}
    $(NF-1) ~ /^[0-9]+$/ {n = $(NF-1)}
    {print $3, n, $0}
' foo.txt \
| sort -k1,1 -k2,2n \
| cut -f3-

This is a Schwarzian transform:

  • the awk program puts the first word of the filename and the file's number as columns before the file's path
  • the data is sorted, first by name then by number
  • then the new columns are removed.

Outputs

dirA/catC/abc-test-8.txt
dirA/catC/abc-test-9.txt
dirA/catC/abc-test-75.txt
dirA/catC/abc-test-999.txt
dirA/catA/addition.txt
dirA/catA/apple.txt
dirA/catB/binary.txt
dirA/catB/carry.txt
dirA/catA/difference
dirA/catB/digit
dirA/catC/test-2.txt
dirA/catC/test-5.txt
dirA/catC/test-7.txt
dirA/catC/test-8.txt
dirA/catC/test-10.txt
dirA/catC/test-11.txt
dirA/catC/test-20.txt
dirA/catC/test-25.txt
dirA/catC/test-50.txt
dirA/catC/test-75.txt
dirA/catC/test-100.txt
dirA/catC/test-500.txt
dirA/catC/test-1000.txt

Same process with a perl one-liner (except that you read perl statements "bottom-up")

perl -e '
  print join "",
        map  { $_->[2] }
        sort { $a->[0] cmp $b->[0] || $a->[1] <=> $b->[1] }
        map  { [m{.*/(\D+)(\d*)}, $_] }
        <>;
' foo.txt
3

With sed:

cat /tmp/foo.txt | sed "s/[[:alnum:]-]*\/[[:alnum:]-]*\/\([[:alpha:]-]*\)\([[:digit:]]*\).*/\0|\1|\2 /"|sort -t"|" -k2,2 -k3n|sed "s/\([^|]*\).*/\1/"

The trick is to temporarily put the desired fields to the end of the lines.

Oops: it's better:

cat source | sed "s/[^/]*\/[^/]*\/\([^[:digit:]]*\)\([[:digit:]]*\).*/\0|\1|\2 /"|sort -t"|" -k2,2 -k3n|sed "s/\([^|]*\).*/\1/"

I changed a little the original question. Sort order the last name, without numbers.

dirA/catC/abc-test-8.txt
dirA/catC/abc-test-9.txt
dirA/catC/abc-test-75.txt
dirA/catC/abc-test-999.txt
dirA/catA/addition.txt
dirA/catA/apple.txt
dirA/catB/binary.txt
dirA/catB/carry.txt
dirA/catA/difference
dirA/catB/digit
dirA/catC/test-2.txt
dirA/catC/test-5.txt
dirA/catC/test-7.txt
dirA/catC/test-8.txt
dirA/catC/test-10.txt
dirA/catC/subdir/test-11.txt
dirA/catC/test-11.txt
dirA/cat C/subdir/test-12.txt
dirA/catC/test-20.txt
dirA/catC/test-25.txt
dirA/catC/test-50.txt
dirA/catC/test-75.txt
dirA/catC/test-100.txt
dirA/catC/test-500.txt
dirA/catC/test-1000.txt
cat /tmp/foo.txt | sed "s/\([^/]*\/\)\+\([^[:digit:]]*\)\([[:digit:]]*\)\(.*\)/\0|\2\4|\3 /"|sort -t"|" -k2,2 -k3n|sed "s/\([^|]*\).*/\1/"

Output:

dirA/catC/abc-test-8.txt
dirA/catC/abc-test-9.txt
dirA/catC/abc-test-75.txt
dirA/catC/abc-test-999.txt
dirA/catA/addition.txt
dirA/catA/apple.txt
dirA/catB/binary.txt
dirA/catB/carry.txt
dirA/catA/difference
dirA/catB/digit
dirA/catC/test-2.txt
dirA/catC/test-5.txt
dirA/catC/test-7.txt
dirA/catC/test-8.txt
dirA/catC/test-10.txt
dirA/catC/subdir/test-11.txt
dirA/catC/test-11.txt
dirA/cat C/subdir/test-12.txt
dirA/catC/test-20.txt
dirA/catC/test-25.txt
dirA/catC/test-50.txt
dirA/catC/test-75.txt
dirA/catC/test-100.txt
dirA/catC/test-500.txt
dirA/catC/test-1000.txt

Explain: \([^/]*\/\)\+ Cut the whole path. =>\1

\([^[:digit:]]*\) file name part without digits =>\2

\([[:digit:]]*\) digits =>3 \(.*\) extension =>4

\0|\2\4|\3 Print the whole line | file name 1st part and the extension | digits

sort -t"|" -k2,2 -k3n|sed "s/\([^|]*\).*/\1/ sort, and cut the unnecessary parts.

Instead of last sed cut -d "|" -f1 also works

2
  • Unfortunately, this does not work for me on Mac OS 11.4. Sorry, should have mentioned that in the question, but perhaps it will be useful to others. Jan 25, 2022 at 17:26
  • Hmmmm. Why not? What is the cat /tmp/foo.txt | sed "s/[^/]*\/[^/]*\/([^[:digit:]]*)([[:digit:]]*).*/\0|\1|\2 /" output?
    – K-attila-
    Jan 26, 2022 at 10:04

You must log in to answer this question.

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