Mengurutkan array nama path file berdasarkan nama dasarnya

8

Misalkan saya memiliki daftar nama path file yang disimpan dalam array

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

Saya ingin mengurutkan elemen-elemen dalam array sesuai dengan nama dasar dari nama file, dalam urutan numerik

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

Bagaimana saya bisa melakukan itu?

Saya hanya bisa mengurutkan bagian nama dasarnya:

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

Saya sedang memikirkan

  • membuat array asosiatif yang kuncinya adalah nama-dasar dan nilainya adalah nama jalur, sehingga akses ke nama jalur selalu dilakukan melalui nama-nama dasar.
  • membuat array lain hanya untuk nama nama, dan berlaku sortuntuk array nama nama.

Terima kasih.

Tim
sumber
1
Itu bukan ide yang baik, tetapi Anda dapat mengurutkan dalam bash
Jeff Schaller
Hati-hati dengan larik yang diketikkan pada nama-nama dasarnya, jika Anda dapat memiliki dir1 / 42.pdf dan dir2 / 42.pdf
Jeff Schaller
Itu (nama path yang berbeda dengan nama yang sama) tidak terjadi dalam kasus saya. Tetapi jika skrip bash dapat mengatasinya, itu akan bagus. Saya tidak memiliki persyaratan yang cukup baik tentang cara mengurutkan nama path dengan nama yang sama, mungkin orang lain mungkin. dir1 dir2hanya dibuat, dan mereka sebenarnya nama path yang sewenang-wenang.
Tim

Jawaban:

4

Berlawanan dengan ksh atau zsh, bash tidak memiliki dukungan bawaan untuk menyortir array atau daftar string yang arbitrer. Hal ini dapat mengurutkan gumpalan atau output dari aliasatau setatau typeset(meskipun mereka terakhir 3 tidak di lokal menyortir agar pengguna), tapi itu tidak dapat digunakan secara praktis di sini.

Tidak ada dalam toolchest POSIX yang dapat dengan mudah mengurutkan daftar string yang acak¹ ( sortmengurutkan baris, jadi hanya pendek (LINE_MAX yang lebih pendek dari PATH_MAX) urutan karakter selain NUL dan baris baru, sedangkan jalur file adalah urutan byte kosong yang tidak kosong lainnya. dari 0).

Jadi, sementara Anda bisa menerapkan algoritma pengurutan Anda sendiri di awk(menggunakan <operator perbandingan string) atau bahkanbash (menggunakan [[ < ]]), untuk jalur sewenang-wenang di bash, mudah dibawa, yang paling mudah adalah dengan menggunakan perl:

Dengan bash4.4+, Anda bisa melakukan:

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

Itu memberi strcmp()perintah seperti. Untuk pesanan yang didasarkan pada aturan pengumpulan lokal seperti di gumpalan atau keluaran dari ls, tambahkan -Mlocaleargumen ke perl. Untuk pengurutan numerik (lebih seperti GNU sort -gkarena mendukung angka seperti +3, 1.2e-5dan bukan ribuan pemisah, meskipun bukan heksadesimal), gunakan <=>sebagai ganti cmp(dan lagi -Mlocaleuntuk tanda desimal pengguna agar dihormati seperti untuk sortperintah).

Anda akan dibatasi oleh ukuran maksimum argumen untuk suatu perintah. Untuk menghindarinya, Anda bisa meneruskan daftar file ke perlstdin alih-alih melalui argumen:

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

Dengan versi yang lebih lama bash, Anda bisa menggunakan while IFS= read -rd ''perulangan alih-alih readarray -d ''atau perlmenampilkan keluaran jalur yang dikutip dengan benar sehingga Anda dapat meneruskannya eval "array=($(perl...))".

Dengan zsh, Anda bisa memalsukan ekspansi glob yang dapat Anda tentukan urutan pengurutan:

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

Dengan reply=($filearray)kami benar-benar memaksa ekspansi glob (yang awalnya hanya /) untuk menjadi elemen array. Kemudian kita menentukan urutan pengurutan berdasarkan pada nama file.

Untuk strcmp()pesanan seperti, perbaiki lokal ke C. Untuk jenis numerik (mirip dengan GNU sort -V, bukan sort -nyang membuat perbedaan signifikan ketika membandingkan 1.4dan 1.23(di lokal di mana .tanda desimal) misalnya), tambahkan nkualifikasi glob.

Alih-alih oe{expression}, Anda juga dapat menggunakan fungsi untuk menentukan urutan pengurutan seperti:

by_tail() REPLY=$REPLY:t

atau yang lebih maju seperti:

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

(Jadi a/foo2bar3.pdf(2,3 angka) diurutkan setelah b/bar1foo3.pdf(1,3) tetapi sebelumnya c/baz2zzz10.pdf(2,10)) dan digunakan sebagai:

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

Tentu saja, itu dapat diterapkan pada gumpalan nyata karena itulah tujuan utamanya. Misalnya, untuk daftar pdffile dalam direktori apa pun, diurutkan berdasarkan nama file / ekor:

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

¹ Jika strcmp()pengurutan berbasis-dapat diterima, dan untuk string pendek, Anda dapat mengubah string ke hex-encoding dengan awksebelum melewati sortdan mengubah kembali setelah pengurutan.

Stéphane Chazelas
sumber
Lihat jawaban ini di bawah ini untuk bash one-liner yang bagus: unix.stackexchange.com/a/394166/41735
kael
9

sortdi GNU coreutils memungkinkan pemisah dan kunci bidang kustom. Anda menetapkan /pemisah bidang dan mengurutkan berdasarkan bidang kedua untuk mengurutkan pada nama dasar, bukan seluruh jalur.

printf "%s\n" "${filearray[@]}" | sort -t/ -k2 akan menghasilkan

dir2/0003.pdf
dir1/0010.pdf
dir3/0040.pdf
Gowtham
sumber
4
Ini adalah opsi standar untuk sort, bukan ekstensi GNU. Ini akan bekerja jika jalurnya semua memiliki panjang yang sama.
Kusalananda
Jawaban yang sama dalam waktu yang bersamaan :)
MiniMax
2
Ini hanya berfungsi jika jalur masing-masing berisi direktori tunggal. Bagaimana dengan some/long/path/0011.pdf? Sejauh yang saya bisa lihat dari halaman manualnya, sorttidak mengandung opsi untuk mengurutkan berdasarkan bidang terakhir.
Federico Poloni
5

Mengurutkan dengan ekspresi gawk (didukung oleh bash 's readarray):

Contoh array nama file yang mengandung spasi putih :

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}')

Hasil:

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

Mengakses satu item:

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

Itu mengasumsikan bahwa tidak ada jalur file berisi karakter baris baru. Perhatikan bahwa penyortiran numerik dari nilai-nilai @val_num_aschanya berlaku untuk bagian numerik utama kunci (tidak ada dalam contoh ini) dengan perbandingan mundur ke leksikal (berdasarkan strcmp(), bukan urutan penyortiran lokal) untuk ikatan.

RomanPerekhrest
sumber
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

Menyortir nama file dengan baris baru di namanya akan menyebabkan masalah pada sortlangkah tersebut.

Ini menghasilkan /daftar -disunting dengan awkyang berisi nama samaran di kolom pertama dan jalur lengkap sebagai kolom yang tersisa:

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

Ini adalah apa yang diurutkan, dan cutdigunakan untuk menghapus /kolom yang telah direvisi pertama . Hasilnya diubah menjadi basharray baru .

Kusalananda
sumber
@ StéphaneChazelas Agak berbulu, tapi ok ...
Kusalananda
Perhatikan bahwa bisa dibilang, itu menghitung nama dasar yang salah untuk path seperti /some/dir/.
Stéphane Chazelas
@ StéphaneChazelas Ya, tetapi OP secara khusus mengatakan ia memiliki path file, jadi saya hanya akan berasumsi bahwa ada nama bas yang tepat di ujung path.
Kusalananda
Perhatikan bahwa dalam GNU non-C lokal yang khas, a/x.c++ b/x.c-- c/x.c++akan disortir dalam urutan itu meskipun -jenis sebelumnya +karena -, +dan /bobot utama adalah IGNORE (jadi membandingkan x.c++/a/x.c++terhadap x.c--/b/x.c++membandingkan pertama xcaxcmelawan xcbxc, dan hanya dalam kasus ikatan akan bobot lainnya (di mana -datang sebelum +) akan dipertimbangkan
Stéphane Chazelas
Itu bisa diselesaikan dengan bergabung /x/bukan /, tapi itu tidak akan mengatasi kasus di mana di C locale pada sistem berbasis ASCII, a/fooakan mengurutkan setelah a/foo.txtmisalnya karena /jenis setelah ..
Stéphane Chazelas
4

Karena " dir1dan dir2nama path arbitrer", kami tidak dapat mengandalkannya yang terdiri dari satu direktori (atau jumlah direktori yang sama). Jadi kita perlu mengkonversi slash terakhir pada nama path ke sesuatu yang tidak terjadi di tempat lain di pathname. Andaikan karakter @tidak muncul di data Anda, Anda dapat mengurutkan berdasarkan nama nama seperti ini:

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

sedPerintah pertama menggantikan garis miring terakhir di setiap pathname dengan pemisah yang dipilih, yang kedua membalikkan perubahan. (Untuk kesederhanaan, saya mengasumsikan nama path dapat dikirimkan satu per baris. Jika mereka ada dalam variabel shell, ubah dulu ke format satu per baris.)

Alexis
sumber
Ha! Ini bagus! Aku membuatnya sedikit lebih kuat (dan sedikit lebih jelek) oleh subbing karakter non-menampilkan seperti: cat pathnames | sed 's|\(.*\)/|\1'$'\4''|' | sort -t$'\4' -k+2nr | sed 's|'$'\4''|/|'. (Saya baru saja meraih \4dari tabel ascii. Rupanya "AKHIR DARI TEKS"?)
kael
@kael, \4adalah ^D(kontrol-D). Kecuali Anda mengetiknya sendiri di terminal, itu adalah karakter kontrol biasa. Dengan kata lain, aman digunakan dengan cara ini.
Alex
3

Solusi singkat (dan agak cepat): Dengan menambahkan indeks array ke nama file dan mengurutkannya, kita kemudian dapat membuat versi yang diurutkan berdasarkan indeks yang diurutkan.

Solusi ini hanya membutuhkan bash builtins serta sortbiner, dan juga berfungsi dengan semua nama file yang tidak menyertakan \nkarakter baris baru .

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

Untuk setiap file, kami mengulangi nama dasarnya dengan indeks awal ditambahkan seperti ini:

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

dan kemudian dikirim sort -n.

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

Setelah itu kita beralih pada jalur output, ekstrak indeks lama dengan ekspansi variabel bash ${line##* }dan masukkan elemen ini ke akhir array baru.

nyronium
sumber
1
Memberi +1 untuk solusi yang tidak perlu melewati nama lengkap setiap file untuk disortir
roaima
3

Ini mengurutkan dengan memprioritaskan nama path file dengan nama file, mengurutkannya secara numerik, dan kemudian menghapus nama file dari bagian depan string:

#!/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

Akan lebih efisien jika Anda memiliki nama file dalam daftar yang dapat dilewatkan secara langsung melalui pipa daripada sebagai array shell, karena pekerjaan yang sebenarnya dilakukan oleh sed | sort | sedstruktur, tetapi ini sudah cukup.

Saya pertama kali menemukan teknik ini ketika coding di Perl; dalam bahasa itu dikenal sebagai Schwartzian Transform .

Di Bash, transformasi seperti yang diberikan di sini dalam kode saya akan gagal jika Anda memiliki non-numerik dalam nama file. Dalam Perl itu bisa dikodekan jauh lebih aman.

roaima
sumber
Terima kasih. apa itu "daftar" di bash? Apakah berbeda dari bash array? Saya tidak pernah mendengarnya dan itu akan menjadi luar biasa. ya, menyimpan nama file dalam "daftar" bisa menjadi ide yang bagus. Saya mendapatkan nama file sebagai $@atau $*dari argumen baris perintah untuk menjalankan skrip
Tim
Menyimpan nama file dalam file memungkinkan utilitas eksternal, tetapi juga berisiko salah tafsir, misalnya, baris baru.
Jeff Schaller
Apakah Schwartzian Transform digunakan dalam menyortir beberapa jenis pola desain, misalnya pola, strategi, ... pola, seperti yang diperkenalkan dalam buku Pola Desain oleh Gang of Four?
Tim
@JeffSchaller untungnya tidak ada baris baru dalam angka. Jika saya menulis kode aman nama file yang sepenuhnya generik, saya sangat mungkin tidak akan menggunakan bash.
roaima
3

Untuk nama file dengan kedalaman yang sama.

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

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

Penjelasan

-k POS1 [, POS2] - Opsi yang disarankan, POSIX, untuk menentukan bidang isian. Bidang terdiri dari bagian garis antara POS1 dan POS2 (atau akhir baris, jika POS2 dihilangkan), termasuk . Kolom dan posisi karakter diberi nomor mulai dengan 1. Jadi untuk mengurutkan pada kolom kedua, Anda akan menggunakan `-k 2,2 '.

-t SEPARATOR Gunakan karakter SEPARATOR sebagai pemisah bidang saat menemukan tombol sortir di setiap baris. Secara default, bidang dipisahkan oleh string kosong antara karakter non-spasi dan karakter spasi.

Informasi diambil dari orang semacam itu.

Pencetakan array yang dihasilkan

printf "%s\n" "${sorted_file_array[@]}"
dir2/0003.pdf
dir1/0010.pdf
dir3/0014.pdf
dir3/0040.pdf
MiniMax
sumber