Ordina una matrice di nomi di percorso dei file in base al loro nome di base


8

Supponiamo di avere un elenco di percorsi di file archiviati in un array

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

Voglio ordinare gli elementi nell'array in base ai nomi di base dei nomi di file, in ordine numerico

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

Come lo posso fare?

Posso solo ordinare le loro parti del nome di base:

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

Ci sto pensando

  • la creazione di un array associativo le cui chiavi sono i nomi di base e i valori sono i nomi dei percorsi, quindi l'accesso ai nomi dei percorsi avviene sempre tramite nomi di base.
  • creando un altro array solo per i nomi di base e applicarlo sortall'array basename.

Grazie.


1
Non è una buona idea, ma puoi ordinare in bash
Jeff Schaller

Attento con una matrice digitata sui nomi di base, se si potesse avere dir1 / 42.pdf e dir2 / 42.pdf
Jeff Schaller

Quello (nomi di percorsi diversi con lo stesso nome di base) non accade nel mio caso. Ma se uno script bash può gestirlo, sarebbe fantastico. Non ho requisiti ragionevolmente buoni su come ordinare i percorsi con lo stesso nome di base, forse qualcun altro potrebbe. dir1 dir2sono appena inventati e in realtà sono nomi di percorso arbitrari.
Tim

Risposte:


4

Contrariamente a ksh o zsh, bash non ha supporto incorporato per l'ordinamento di matrici o elenchi di stringhe arbitrarie. Può ordinare globs o l'output di aliaso seto typeset(sebbene quelli ultimi 3 non siano nell'ordinamento locale dell'utente), ma non possono essere usati praticamente qui.

Non c'è nulla nel toolchest POSIX che può facilmente ordinare elenchi arbitrari di stringhe¹ ( sortordina le linee, quindi solo brevi (LINE_MAX essendo spesso più brevi di PATH_MAX) sequenze di caratteri diversi da NUL e newline, mentre i percorsi dei file sono sequenze non vuote di byte altri di 0).

Quindi, mentre potresti implementare il tuo algoritmo di ordinamento in awk(usando l' <operatore di confronto delle stringhe) o anchebash (usando [[ < ]]), per percorsi arbitrari in bash, portabilmente, il più semplice potrebbe essere ricorrere a perl:

Con bash4.4+, potresti fare:

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

Questo dà un strcmp()ordine simile. Per un ordine basato sulle regole di confronto della locale come in globs o l'output di ls, aggiungi un -Mlocaleargomento a perl. Per l'ordinamento numerico (più simile a GNU sort -gin quanto supporta numeri come +3, 1.2e-5e non migliaia di separatori, sebbene non esadimali), utilizzare <=>invece di cmp(e ancora -Mlocaleper il segno decimale dell'utente da onorare come per il sortcomando).

Sarai limitato dalla dimensione massima degli argomenti a un comando. Per evitarlo, potresti passare l'elenco dei file perlsul suo stdin invece che tramite argomenti:

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

Con le versioni precedenti di bash, è possibile utilizzare un while IFS= read -rd ''ciclo anziché readarray -d ''o ottenere perll'output dell'elenco di percorsi correttamente citato in modo da poterlo passare eval "array=($(perl...))".

Con zsh, puoi simulare un'espansione glob per la quale puoi definire un ordinamento:

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

Con reply=($filearray)forziamo l'espansione glob (che inizialmente era giusta /) per essere gli elementi dell'array. Quindi definiamo l'ordinamento in base alla coda del nome file.

Per un strcmp()ordine simile, fissa le impostazioni locali su C. Per l'ordinamento numerico (simile a GNU sort -V, sort -nche non fa una differenza significativa durante il confronto 1.4e 1.23(in locali dove .è il segno decimale) per esempio), aggiungi il nqualificatore glob.

Invece di oe{expression}, puoi anche usare una funzione per definire un ordinamento come:

by_tail() REPLY=$REPLY:t

o più avanzati come:

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

(quindi a/foo2bar3.pdf(2,3 numeri) ordina dopo b/bar1foo3.pdf(1,3) ma prima c/baz2zzz10.pdf(2,10)) e utilizza come:

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

Naturalmente, quelli possono essere applicati su globs reali in quanto è principalmente destinato. Ad esempio, per un elenco di pdffile in qualsiasi directory, ordinati per basename / tail:

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

¹ Se un strcmp()ordinamento basato su dati è accettabile e per stringhe brevi, è possibile trasformare le stringhe nella loro codifica esadecimale con awkprima di passare a sorte trasformarle indietro dopo l'ordinamento.


Vedi questa risposta qui sotto per un ottimo bash one-liner: unix.stackexchange.com/a/394166/41735
kael

9

sortin GNU coreutils consente una chiave e un separatore di campi personalizzati. Si imposta /come separatore di campo e si ordina in base al secondo campo per ordinare il nome di base, anziché l'intero percorso.

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

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

4
Questa è un'opzione standard per sort, non un'estensione GNU. Questo funzionerà se i percorsi hanno tutti la stessa lunghezza.
Kusalananda

Stessa risposta contemporaneamente :)
MiniMax

2
Funziona solo se i percorsi contengono una singola directory ciascuno. Che dire some/long/path/0011.pdf? Per quanto posso vedere dalla sua pagina man, sortnon contiene alcuna opzione per ordinare in base all'ultimo campo.
Federico Poloni,

5

Ordinamento con espressione gawk (supportata da bash 's readarray):

Matrice di esempio di nomi di file contenenti spazi bianchi :

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

Il risultato:

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

Accesso a singolo elemento:

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

Ciò presuppone che nessun percorso di file contenga caratteri di nuova riga. Si noti che l'ordinamento numerico dei valori @val_num_ascsi applica solo alla parte numerica iniziale della chiave (nessuna in questo esempio) con fallback al confronto lessicale (basato su strcmp(), non sull'ordinamento della locale) per i legami.


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

L'ordinamento dei nomi dei file con le nuove righe nei loro nomi causerà problemi al sortpassaggio.

Genera un /elenco -delimitato con awkquello che contiene il nome di base nella prima colonna e il percorso completo come le colonne rimanenti:

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

Questo è ciò che viene ordinato e cutviene utilizzato per rimuovere la prima /colonna delimitata. Il risultato viene trasformato in un nuovo basharray.


@ StéphaneChazelas Un po 'peloso, ma ok ...
Kusalananda

Si noti che, probabilmente, calcola il basename errato per percorsi come /some/dir/.
Stéphane Chazelas,

@ StéphaneChazelas Sì, ma l'OP ha espressamente affermato che aveva percorsi di file, quindi suppongo che alla fine del percorso sia presente un nome di base appropriato.
Kusalananda

Si noti che in una tipica locale GNU non C, a/x.c++ b/x.c-- c/x.c++verrebbero ordinati in quell'ordine anche se -prima ordina +perché -, +e /il peso primario è IGNORA (quindi il confronto x.c++/a/x.c++con il x.c--/b/x.c++primo confronto con il xcaxcconfronto xcbxc, e solo in caso di legami sarebbero gli altri pesi (dove -viene prima +))
Stéphane Chazelas,

Ciò potrebbe essere aggirato unendo /x/invece di /, ma non affronterebbe il caso in cui nella locale C su sistemi basati su ASCII, a/foosi ordinasse dopo a/foo.txtper esempio perché /ordina dopo ..
Stéphane Chazelas,

4

Poiché " dir1e dir2sono percorsi arbitrari", non possiamo contare su di essi costituiti da una singola directory (o dello stesso numero di directory). Quindi abbiamo bisogno di convertire l' ultima barra nei nomi dei percorsi in qualcosa che non si trova altrove nel percorso. Supponendo che il carattere @non si verifichi nei tuoi dati, puoi ordinare in base al nome di base in questo modo:

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

Il primo sedcomando sostituisce l' ultima barra in ogni percorso con il separatore scelto, il secondo inverte la modifica. (Per semplicità, suppongo che i nomi dei percorsi possano essere consegnati uno per riga. Se si trovano in una variabile shell, convertirli prima in un formato per riga.)


Ha! Questo è fantastico! Ho fatto leggermente più robusto (e un po 'più brutta) di subbing un carattere non mostrando in questo modo: cat pathnames | sed 's|\(.*\)/|\1'$'\4''|' | sort -t$'\4' -k+2nr | sed 's|'$'\4''|/|'. (Ho appena preso \4dal tavolo ASCII. Apparentemente "FINE DEL TESTO"?)
Kael

@kael, \4is ^D(control-D). A meno che non lo digiti da solo sul terminale, è un normale carattere di controllo. In altre parole, sicuro da usare in questo modo.
alexis,

3

Soluzione breve (e in qualche modo veloce): aggiungendo l'indice dell'array ai nomi dei file e ordinandoli, possiamo successivamente creare una versione ordinata in base alle indicazioni ordinate.

Questa soluzione necessita solo dei builtin bash e sortbinari e funziona anche con tutti i nomi di file che non includono un \ncarattere di nuova riga .

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

Per ogni file, facciamo eco al suo nome di base con il suo indice iniziale aggiunto in questo modo:

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

e poi inviato sort -n.

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

Successivamente eseguiamo l'iterazione sulle linee di output, estraiamo il vecchio indice con l'espansione della variabile bash ${line##* }e inseriamo questo elemento alla fine del nuovo array.


1
+1 per una soluzione che non richiede il passaggio del nome completo di ciascun file per l'ordinamento
roaima

3

Questo ordina anteponendo i nomi dei file al nome di base, ordinandolo numericamente e quindi rimuovendo il nome di base dalla parte anteriore della stringa:

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

Sarebbe più efficiente se i nomi dei file fossero in un elenco che potrebbe essere passato direttamente attraverso una pipe piuttosto che come un array di shell, perché il lavoro effettivo viene svolto dalla sed | sort | sedstruttura, ma questo è sufficiente.

Mi sono imbattuto per la prima volta in questa tecnica durante la programmazione in Perl; in quella lingua era conosciuta come una trasformazione di Schwartz .

In Bash la trasformazione come indicato qui nel mio codice fallirà se hai nomi non numerici nel nome di base del file. In Perl potrebbe essere codificato in modo molto più sicuro.


Grazie. che cos'è un "elenco" in bash? È diverso dall'array bash? Non ne ho mai sentito parlare e sarebbe fantastico. sì, memorizzare i nomi dei file in un "elenco" potrebbe essere una buona idea. Ho ottenuto i nomi dei file come $@o $*da argomenti della riga di comando per l'esecuzione di uno script
Tim

La memorizzazione dei nomi dei file in un file consente l'utilizzo di utility esterne, ma rischia anche di interpretare erroneamente, per esempio, le nuove righe.
Jeff Schaller

La trasformazione di Schwartz è utilizzata per ordinare una sorta di modello di progettazione, ad esempio modello, strategia, ... modelli, come introdotto nel libro Design Pattern di Gang of Four?
Tim

@JeffSchaller per fortuna non ci sono nuove linee numeriche. Se stavo scrivendo un codice completamente generico sicuro per i nomi di file, probabilmente non userei bash.
roaima,

3

Per nomi di file di uguale profondità.

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

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

Spiegazione

-k POS1 [, POS2] - L'opzione POSIX consigliata per specificare un campo di ordinamento. Il campo è costituito dalla parte della linea tra POS1 e POS2 (o la fine della linea, se POS2 è omessa), incluso . I campi e le posizioni dei caratteri sono numerati a partire da 1. Quindi per ordinare sul secondo campo, dovresti usare `-k 2,2 '.

-t SEPARATORE Usa il carattere SEPARATORE come separatore di campo quando trovi le chiavi di ordinamento in ogni riga. Per impostazione predefinita, i campi sono separati da una stringa vuota tra un carattere non bianco e uno bianco.

Le informazioni sono tratte dall'uomo del tipo.

La stampa di array risultante

printf "%s\n" "${sorted_file_array[@]}"
dir2/0003.pdf
dir1/0010.pdf
dir3/0014.pdf
dir3/0040.pdf
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.