Sắp xếp một mảng các tên đường dẫn của tệp theo tên cơ sở của chúng


8

Giả sử rằng tôi có danh sách tên đường dẫn của các tệp được lưu trữ trong một mảng

filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" ) 

Tôi muốn sắp xếp các phần tử trong mảng theo tên cơ sở của tên tệp, theo thứ tự số

sortedfilearray=("dir2/0003.pdf" "dir1/0010.pdf" "dir3/0040.pdf") 

Làm thế nào tôi có thể làm điều đó?

Tôi chỉ có thể sắp xếp các phần tên cơ sở của họ:

basenames=()
for file in "${filearray[@]}"
do
    filename=${file##*/}
    basenames+=(${filename%.*})
done
sortedbasenamearr=($(printf '%s\n' "${basenames[@]}" | sort -n))

Tôi đang suy nghĩ về

  • tạo một mảng kết hợp có khóa là tên cơ sở và giá trị là tên đường dẫn, vì vậy việc truy cập vào tên đường dẫn luôn được thực hiện thông qua tên cơ sở.
  • tạo một mảng khác chỉ cho tên cơ sở và áp dụng sortcho mảng tên cơ sở.

Cảm ơn.


1
Đó không phải là một ý tưởng tốt, nhưng bạn có thể sắp xếp trong bash
Jeff Schaller

Cẩn thận với một mảng được khóa trên các tên cơ sở, nếu bạn có thể có dir1 / 42.pdf và dir2 / 42.pdf
Jeff Schaller

Điều đó (tên đường dẫn khác nhau có cùng tên cơ sở) không xảy ra trong trường hợp của tôi. Nhưng nếu một kịch bản bash có thể đối phó với nó, điều đó sẽ rất tuyệt. Tôi không có yêu cầu khá tốt về cách sắp xếp tên đường dẫn với cùng tên cơ sở, có thể người khác có thể. dir1 dir2chỉ được tạo thành, và chúng thực sự là tên đường dẫn tùy ý.
Tim

Câu trả lời:


4

Trái với ksh hoặc zsh, bash không có hỗ trợ dựng sẵn để sắp xếp các mảng hoặc danh sách các chuỗi tùy ý. Nó có thể sắp xếp những đống hoặc đầu ra của aliashay sethay typeset(mặc dù những cuối cùng 3 không trong thứ tự sắp xếp locale của người dùng), nhưng điều đó có thể không được sử dụng thực tế ở đây.

Không có gì trong bộ công cụ POSIX có thể dễ dàng sắp xếp danh sách các chuỗi tùy ý ( sortsắp xếp các dòng, do đó, chỉ các chuỗi ký tự ngắn (LINE_MAX thường ngắn hơn PATH_MAX) ngoài NUL và dòng mới, trong khi các đường dẫn tệp không phải là chuỗi byte khác hơn 0).

Vì vậy, trong khi bạn có thể thực hiện thuật toán sắp xếp của riêng mình trong awk(sử dụng <toán tử so sánh chuỗi) hoặc thậm chíbash (sử dụng [[ < ]]), đối với các đường dẫn tùy ý bash, có thể dễ dàng nhất là sử dụng perl:

Với bash4.4+, bạn có thể làm:

readarray -td '' sorted_filearray < <(perl -MFile::Basename -l0 -e '
  print for sort {basename($a) cmp basename($b)} @ARGV' -- "${filearray[@]}")

Điều đó mang lại một strcmp()trật tự giống như. Đối với một đơn hàng dựa trên các quy tắc đối chiếu của miền địa phương như trong các khối hoặc đầu ra của ls, hãy thêm một -Mlocaleđối số vào perl. Đối với sắp xếp số (giống như GNU sort -gvì nó hỗ trợ các số như +3, 1.2e-5chứ không phải hàng nghìn dấu phân cách, mặc dù không phải là hexadimals), hãy sử dụng <=>thay vì cmp(và một lần nữa -Mlocaleđể dấu thập phân của người dùng được tôn vinh như sortlệnh).

Bạn sẽ bị giới hạn bởi kích thước tối đa của các đối số cho một lệnh. Để tránh điều đó, bạn có thể chuyển danh sách các tệp perlvào stdin của nó thay vì thông qua các đối số:

readarray -td '' sorted_filearray < <(
  printf '%s\0' "${filearray[@]}" | perl -MFile::Basename -0le '
    chomp(@files = <STDIN>);
    print for sort {basename($a) cmp basename($b)} @files')

Với các phiên bản cũ hơn bash, bạn có thể sử dụng while IFS= read -rd ''vòng lặp thay vì readarray -d ''hoặc perlxuất ra danh sách các đường dẫn được trích dẫn chính xác để bạn có thể chuyển nó tới eval "array=($(perl...))".

Với zsh, bạn có thể giả mạo một bản mở rộng toàn cầu mà bạn có thể xác định thứ tự sắp xếp:

sorted_filearray=(/(e{'reply=($filearray)'}oe{'REPLY=$REPLY:t'}))

Với reply=($filearray)chúng tôi thực sự buộc việc mở rộng toàn cầu (mà ban đầu chỉ là /) là các yếu tố của mảng. Sau đó, chúng tôi xác định thứ tự sắp xếp dựa trên đuôi của tên tệp.

Đối với một strcmp()thứ tự giống như, sửa lỗi miền địa phương thành C. Đối với sắp xếp số (tương tự GNU sort -V, không sort -ntạo ra sự khác biệt đáng kể khi so sánh 1.41.23( .ví dụ, trong đó là dấu thập phân), hãy thêm vào nvòng loại toàn cầu.

Thay vì oe{expression}, bạn cũng có thể sử dụng một hàm để xác định thứ tự sắp xếp như:

by_tail() REPLY=$REPLY:t

hoặc những cái cao cấp hơn như:

by_numbers_in_tail() REPLY=${(j:,:)${(s:,:)${REPLY:t}//[^0-9]/,}}

(vì vậy a/foo2bar3.pdf(2,3 số) sắp xếp sau b/bar1foo3.pdf(1,3) nhưng trước c/baz2zzz10.pdf(2,10)) và sử dụng như:

sorted_filearray=(/(e{'reply=($filearray)'}no+by_numbers_in_tail))

Tất nhiên, những thứ đó có thể được áp dụng trên các quả bóng thực sự vì đó là những gì chúng chủ yếu dành cho. Chẳng hạn, đối với danh sách các pdftệp trong bất kỳ thư mục nào, được sắp xếp theo tên cơ sở / đuôi:

pdfs=(**/*.pdf(N.oe+by_tail))

Nếu strcmp()việc sắp xếp dựa trên cơ sở có thể chấp nhận được và đối với các chuỗi ngắn, bạn có thể chuyển đổi các chuỗi thành mã hóa hex của chúng awktrước khi chuyển đến sortvà chuyển đổi trở lại sau khi sắp xếp.


Xem câu trả lời dưới đây để có một bash one-liner tuyệt vời: unix.stackexchange.com/a/394166/41735
kael

9

sorttrong GNU coreutils cho phép phân tách trường và khóa tùy chỉnh. Bạn đặt /làm dấu tách trường và sắp xếp dựa trên trường thứ hai để sắp xếp trên tên cơ sở, thay vì toàn bộ đường dẫn.

printf "%s\n" "${filearray[@]}" | sort -t/ -k2 sẽ sản xuất

dir2/0003.pdf
dir1/0010.pdf
dir3/0040.pdf

4
Đây là một tùy chọn tiêu chuẩn cho sort, không phải là một phần mở rộng GNU. Điều này sẽ hoạt động nếu tất cả các đường dẫn có cùng độ dài.
Kusalananda

Cùng một câu trả lời cùng một lúc :)
MiniMax

2
Điều này chỉ hoạt động nếu các đường dẫn chứa một thư mục duy nhất mỗi. Thế còn some/long/path/0011.pdf? Theo như tôi có thể thấy từ trang man của nó, sortkhông chứa tùy chọn nào để sắp xếp theo trường cuối cùng.
Federico Poloni

5

Sắp xếp với biểu thức gawk (được hỗ trợ bởi bash ' readarray):

Mảng mẫu tên tệp chứa khoảng trắng :

filearray=("dir1/name 0010.pdf" "dir2/name  0003.pdf" "dir3/name 0040.pdf")

readarray -t sortedfilearr < <(printf '%s\n' "${filearray[@]}" | awk -F'/' '
   BEGIN{PROCINFO["sorted_in"]="@val_num_asc"}
   { a[$0]=$NF }
   END{ for(i in a) print i}')

Đầu ra:

echo "${sortedfilearr[*]}"
dir2/name 0003.pdf dir1/name 0010.pdf dir3/name 0040.pdf

Truy cập mục duy nhất:

echo "${sortedfilearr[1]}"
dir1/name 0010.pdf

Điều đó giả định rằng không có đường dẫn tệp nào chứa các ký tự dòng mới. Lưu ý rằng việc sắp xếp số của các giá trị @val_num_ascchỉ áp dụng cho phần số hàng đầu của khóa (không có trong ví dụ này) với dự phòng so sánh từ vựng (dựa trên strcmp(), không phải thứ tự sắp xếp của miền địa phương) cho các mối quan hệ.


4
oldIFS="$IFS"; IFS=$'\n'
if [[ -o noglob ]]; then
  setglob=1; set -o noglob
else
  setglob=0
fi

sorted=( $(printf '%s\n' "${filearray[@]}" |
            awk '{ print $NF, $0 }' FS='/' OFS='/' |
            sort | cut -d'/' -f2- ) )

IFS="$oldIFS"; unset oldIFS
(( setglob == 1 )) && set +o noglob
unset setglob

Sắp xếp tên tệp với dòng mới trong tên của chúng sẽ gây ra vấn đề ở sortbước này.

Nó tạo ra một /danh sách giới hạn với awkchứa tên cơ sở trong cột đầu tiên và đường dẫn đầy đủ như các cột còn lại:

0003.pdf/dir2/0003.pdf
0010.pdf/dir1/0010.pdf
0040.pdf/dir3/0040.pdf

Đây là những gì được sắp xếp và cutđược sử dụng để loại bỏ /cột được phân tách đầu tiên . Kết quả được biến thành một bashmảng mới .


@ StéphaneChazelas Một chút lông, nhưng ok ...
Kusalananda

Lưu ý rằng có thể cho rằng, nó tính toán tên cơ sở sai cho các đường dẫn như /some/dir/.
Stéphane Chazelas

@ StéphaneChazelas Có, nhưng OP đặc biệt nói rằng anh ta có đường dẫn của các tệp, vì vậy tôi sẽ giả sử rằng có một tên cơ sở thích hợp ở cuối đường dẫn.
Kusalananda

Lưu ý rằng trong một GNU phi C locale điển hình, a/x.c++ b/x.c-- c/x.c++sẽ được sắp xếp theo thứ tự mà mặc dù -loại trước +bởi vì -, +/'s cân chính là bỏ qua (để so sánh x.c++/a/x.c++đối x.c--/b/x.c++đầu so sánh xcaxcvới xcbxc, và chỉ trong trường hợp quan hệ sẽ trọng khác (nơi -đến trước +) sẽ được xem xét.
Stéphane Chazelas

Điều đó có thể được giải quyết bằng cách tham gia /x/thay vì /, nhưng điều đó sẽ không giải quyết được trường hợp trong ngôn ngữ C trên các hệ thống dựa trên ASCII, a/foosẽ sắp xếp theo sau a/foo.txt/sắp xếp sau ..
Stéphane Chazelas

4

Vì " dir1dir2là tên đường dẫn tùy ý", chúng tôi không thể tin tưởng vào chúng bao gồm một thư mục duy nhất (hoặc có cùng số lượng thư mục). Vì vậy, chúng ta cần chuyển đổi dấu gạch chéo cuối cùng trong tên đường dẫn thành tên không xuất hiện ở nơi khác trong tên đường dẫn. Giả sử ký tự @không xảy ra trong dữ liệu của bạn, bạn có thể sắp xếp theo tên cơ sở như thế này:

cat pathnames | sed 's|\(.*\)/|\1@|' | sort -t@ -k+2 | sed 's|@|/|'

Lệnh đầu tiên sedthay thế dấu gạch chéo cuối cùng trong mỗi tên đường dẫn bằng dấu phân cách đã chọn, lệnh thứ hai đảo ngược thay đổi. (Để đơn giản, tôi giả sử tên đường dẫn có thể được phân phối một dòng trên mỗi dòng. Nếu chúng ở trong một biến shell, trước tiên hãy chuyển đổi chúng thành định dạng một dòng.)


Hà! Điều đó thật tuyệt! Tôi đã làm cho nó mạnh mẽ hơn một chút (và hơi xấu hơn) bằng cách thay thế một nhân vật không hiển thị như vậy : cat pathnames | sed 's|\(.*\)/|\1'$'\4''|' | sort -t$'\4' -k+2nr | sed 's|'$'\4''|/|'. (Tôi vừa lấy \4từ bảng ascii. Rõ ràng là "KẾT THÚC"?)
kael

@kael, \4^D(kiểm soát-D). Trừ khi bạn tự gõ nó ở thiết bị đầu cuối, đó là một nhân vật điều khiển thông thường. Nói cách khác, an toàn để sử dụng theo cách này.
alexis

3

Giải pháp ngắn (và hơi nhanh): Bằng cách nối thêm chỉ mục mảng vào tên tệp và sắp xếp chúng, sau đó chúng ta có thể tạo một phiên bản được sắp xếp dựa trên các chỉ báo được sắp xếp.

Giải pháp này chỉ cần các nội dung bash cũng như sortnhị phân và cũng hoạt động với tất cả các tên tệp không bao gồm ký \ntự dòng mới .

index=0 sortedfilearray=()
while read -r line ; do
    sortedfilearray+=("${filearray[${line##* }]}")
done <<< "$(for i in "${filearray[@]}" ; do
    echo "$(basename "$i") $((index++))"
done | sort -n)"

Đối với mỗi tệp, chúng tôi lặp lại tên cơ sở của nó với chỉ mục ban đầu được nối như sau:

0010.pdf 0
0003.pdf 1
0040.pdf 2

và sau đó gửi qua sort -n.

0003.pdf 1
0010.pdf 0
0040.pdf 2

Sau đó, chúng tôi lặp lại các dòng đầu ra, trích xuất chỉ mục cũ với mở rộng biến bash ${line##* }và chèn phần tử này vào cuối mảng mới.


1
+1 cho một giải pháp không yêu cầu chuyển tên đầy đủ của mỗi tệp để sắp xếp
roaima

3

Điều này sắp xếp bằng cách thêm trước tên đường dẫn tệp với tên cơ sở, sắp xếp số đó và sau đó tước tên cơ sở từ phía trước của chuỗi:

#!/bin/bash
#
filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" "dir4/0003.pdf")

sortarray=($(
    for file in "${filearray[@]}"
    do
        echo "$file"
    done |
        sed -r 's!^(.*)/([[:digit:]]*)(.*)$!\2 \1/\2\3!' |
        sort -t $'\t' -n |
        sed -r 's![^ ]* !!'
))

for item in "${sortarray[@]}"
do
    echo "> $item <"
done

Sẽ hiệu quả hơn nếu bạn có tên tệp trong danh sách có thể được truyền trực tiếp qua đường ống chứ không phải là mảng vỏ, bởi vì công việc thực tế được thực hiện bởi sed | sort | sedcấu trúc, nhưng điều này đủ.

Lần đầu tiên tôi bắt gặp kỹ thuật này khi viết mã bằng Perl; trong ngôn ngữ đó, nó được gọi là Biến đổi Schwartzian .

Trong Bash, biến đổi như được đưa ra ở đây trong mã của tôi sẽ thất bại nếu bạn không có số trong tên cơ sở của tệp. Trong Perl nó có thể được mã hóa an toàn hơn nhiều.


cảm ơn. một "danh sách" trong bash là gì? Có khác với mảng bash không? Tôi chưa bao giờ nghe về nó và nó sẽ rất tuyệt. vâng, lưu trữ tên tệp trong một "danh sách" có thể là một ý tưởng tốt. Tôi đã nhận được tên tệp là $@hoặc $*từ các đối số dòng lệnh để chạy tập lệnh
Tim

Lưu trữ tên tệp trong một tệp cho phép các tiện ích bên ngoài, nhưng cũng có nguy cơ giải thích sai về các dòng mới.
Jeff Schaller

Có phải Schwartzian Transform được sử dụng để sắp xếp một số loại mẫu thiết kế, ví dụ mẫu, chiến lược, ... mẫu, như được giới thiệu trong cuốn sách Design Pattern by Gang of Four?
Tim

@JeffSchaller may mắn thay, không có dòng mới nào về số lượng. Nếu tôi đang viết mã an toàn tên tệp hoàn toàn chung, tôi hoàn toàn có thể sẽ không sử dụng bash.
roaima

3

Đối với tên tệp có độ sâu bằng nhau.

filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" "dir3/0014.pdf")

sorted_file_array=($(printf "%s\n" "${filearray[@]}" | sort -n -t'/' -k2))

Giải trình

-k POS1 [, POS2] - Tùy chọn được đề xuất, POSIX, để chỉ định trường sắp xếp. Trường bao gồm một phần của dòng giữa POS1 và POS2 (hoặc cuối dòng, nếu POS2 bị bỏ qua), bao gồm . Các trường và vị trí ký tự được đánh số bắt đầu bằng 1. Vì vậy, để sắp xếp trên trường thứ hai, bạn sẽ sử dụng '-k 2,2'.

-t SEPARATOR Sử dụng ký tự SEPARATOR làm dấu tách trường khi tìm các khóa sắp xếp trong mỗi dòng. Theo mặc định, các trường được phân tách bằng chuỗi trống giữa ký tự không phải khoảng trắng và ký tự khoảng trắng.

Thông tin được lấy từ người đàn ông sắp xếp.

Kết quả in mảng

printf "%s\n" "${sorted_file_array[@]}"
dir2/0003.pdf
dir1/0010.pdf
dir3/0014.pdf
dir3/0040.pdf
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.