Sortieren Sie ein Array von Pfadnamen von Dateien nach ihren Basisnamen

8

Angenommen, ich habe eine Liste mit Pfadnamen von Dateien, die in einem Array gespeichert sind

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

Ich möchte die Elemente im Array nach den Basisnamen der Dateinamen in numerischer Reihenfolge sortieren

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

Wie kann ich das machen?

Ich kann nur ihre Basisnamen-Teile sortieren:

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

Ich denke an

  • Erstellen eines assoziativen Arrays, dessen Schlüssel die Basisnamen und deren Werte die Pfadnamen sind, sodass der Zugriff auf die Pfadnamen immer über Basisnamen erfolgt.
  • Erstellen eines anderen Arrays nur für Basisnamen und Anwenden sortauf das Basisnamen-Array.

Vielen Dank.

Tim
quelle
1
Es ist keine gute Idee, aber Sie können in Bash sortieren
Jeff Schaller
Vorsicht mit einem Array, das auf den Basisnamen verschlüsselt ist, wenn Sie dir1 / 42.pdf und dir2 / 42.pdf haben könnten
Jeff Schaller
Das (verschiedene Pfadnamen mit demselben Basisnamen) passiert in meinem Fall nicht. Aber wenn ein Bash-Skript damit umgehen kann, ist das großartig. Ich habe keine vernünftigen Anforderungen, wie man Pfadnamen mit demselben Basisnamen sortiert, vielleicht kann es jemand anderes tun. dir1 dir2sind nur erfunden, und sie sind eigentlich willkürliche Pfadnamen.
Tim

Antworten:

4

Im Gegensatz zu ksh oder zsh bietet bash keine integrierte Unterstützung für das Sortieren von Arrays oder Listen beliebiger Zeichenfolgen. Es kann Globs oder die Ausgabe von aliasoder setoder sortieren typeset(obwohl die letzten 3 nicht in der Sortierreihenfolge des Benutzers enthalten sind), aber das kann hier praktisch nicht verwendet werden.

Es gibt nichts in der POSIX-Toolchest, das beliebige Listen von Zeichenfolgen leicht sortieren kann¹ ( sortsortiert Zeilen, also nur kurze (LINE_MAX ist oft kürzer als PATH_MAX) Folgen von Zeichen außer NUL und Newline, während Dateipfade nicht leere Folgen von anderen Bytes sind als 0).

Während Sie also Ihren eigenen Sortieralgorithmus in awk(mithilfe des <Zeichenfolgenvergleichsoperators) oder sogarbash (mithilfe [[ < ]]) implementieren können , ist es für beliebige Pfade bashmöglicherweise am einfachsten, auf Folgendes zurückzugreifen perl:

Mit bash4.4+könnten Sie tun:

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

Das gibt eine strcmp()ähnliche Reihenfolge. Fügen Sie für eine Reihenfolge, die auf den Sortierregeln des Gebietsschemas wie in Globs oder der Ausgabe von basiert ls, ein -MlocaleArgument hinzu perl. Für numerische Sortierung (mehr wie GNU sort -gwie es unterstützt Zahlen wie +3, 1.2e-5und nicht die Tausendertrennzeichen , wenn auch nicht hexadimals), verwenden <=>statt cmp(und wieder -Mlocalefür das Dezimalzeichen des Benutzers wie für die geehrt werden sortBefehl).

Sie sind durch die maximale Größe der Argumente für einen Befehl begrenzt. Um dies zu vermeiden, können Sie die Liste der Dateien perlauf ihrem Standard anstatt über Argumente übergeben:

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

Bei älteren Versionen von bashkönnen Sie eine while IFS= read -rd ''Schleife anstelle von verwenden readarray -d ''oder perldie Liste der ordnungsgemäß zitierten Pfade ausgeben, an die Sie sie übergeben können eval "array=($(perl...))".

Mit zshkönnen Sie eine Glob-Erweiterung vortäuschen, für die Sie eine Sortierreihenfolge definieren können:

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

Mit reply=($filearray)erzwingen wir tatsächlich die Glob-Erweiterung (die anfangs nur war /), um die Elemente des Arrays zu sein. Dann definieren wir die Sortierreihenfolge basierend auf dem Ende des Dateinamens.

strcmp()Legen Sie für eine ähnliche Reihenfolge das Gebietsschema auf C fest. Fügen Sie für eine numerische Sortierung (ähnlich wie bei GNU sort -V, sort -ndie beim Vergleich keinen signifikanten Unterschied macht, 1.4und 1.23(in Gebietsschemas, in denen .die Dezimalstelle steht) beispielsweise) das nGlob-Qualifikationsmerkmal hinzu.

Stattdessen oe{expression}können Sie auch eine Funktion verwenden, um eine Sortierreihenfolge wie folgt zu definieren:

by_tail() REPLY=$REPLY:t

oder fortgeschrittenere wie:

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

(also a/foo2bar3.pdf(2,3 Zahlen) sortiert nach b/bar1foo3.pdf(1,3) aber vor c/baz2zzz10.pdf(2,10)) und verwendet als:

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

Natürlich können diese auf echte Globs angewendet werden, da sie in erster Linie dafür vorgesehen sind. Zum Beispiel für eine Liste von pdfDateien in einem beliebigen Verzeichnis, sortiert nach Basisname / Schwanz:

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

¹ Wenn eine strcmp()sortierte Sortierung akzeptabel ist und für kurze Zeichenfolgen, können Sie die Zeichenfolgen in ihre Hex-Codierung umwandeln, awkbevor Sie sie übergeben sortund nach dem Sortieren wieder umwandeln.

Stéphane Chazelas
quelle
Siehe diese Antwort unten für einen großartigen Bash- Einzeiler
kael
9

sortIn GNU ermöglicht Coreutils ein benutzerdefiniertes Feldtrennzeichen und einen benutzerdefinierten Schlüssel. Sie legen /als Feldtrennzeichen fest und sortieren nach dem zweiten Feld, um nach dem Basisnamen anstatt nach dem gesamten Pfad zu sortieren.

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

dir2/0003.pdf
dir1/0010.pdf
dir3/0040.pdf
Gowtham
quelle
4
Dies ist eine Standardoption für sortkeine GNU-Erweiterung. Dies funktioniert, wenn alle Pfade gleich lang sind.
Kusalananda
Gleiche Antwort zur gleichen Zeit :)
MiniMax
2
Dies funktioniert nur, wenn die Pfade jeweils ein einziges Verzeichnis enthalten. Was ist mit some/long/path/0011.pdf? Soweit ich auf der Manpage sehen kann, sortenthält es keine Option zum Sortieren nach dem letzten Feld.
Federico Poloni
5

Sortieren mit gawk Ausdruck (unterstützt von bash s readarray):

Beispielarray von Dateinamen mit Leerzeichen :

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

Die Ausgabe:

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

Zugriff auf ein einzelnes Element:

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

Dies setzt voraus, dass kein Dateipfad Zeilenumbruchzeichen enthält. Beachten Sie, dass die numerische Sortierung der Werte in @val_num_ascnur für den führenden numerischen Teil des Schlüssels gilt (in diesem Beispiel keine), wobei auf den lexikalischen Vergleich (basierend auf strcmp()der Sortierreihenfolge des Gebietsschemas) für Verknüpfungen zurückgegriffen wird.

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

Das Sortieren von Dateinamen mit Zeilenumbrüchen führt zu Problemen beim sortSchritt.

Es wird eine /begrenzte Liste generiert awk, die den Basisnamen in der ersten Spalte und den vollständigen Pfad als verbleibende Spalten enthält:

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

Dies ist das, was sortiert wird und cutverwendet wird, um die erste /begrenzte Spalte zu entfernen . Das Ergebnis wird in ein neues bashArray umgewandelt.

Kusalananda
quelle
@ StéphaneChazelas Ein bisschen haarig, aber ok ...
Kusalananda
Beachten Sie, dass es wahrscheinlich den falschen Basisnamen für Pfade wie berechnet /some/dir/.
Stéphane Chazelas
@ StéphaneChazelas Ja, aber das OP hat ausdrücklich gesagt, dass er Dateipfade hat, also gehe ich einfach davon aus, dass am Ende des Pfads ein richtiger Basisname steht.
Kusalananda
Man beachte , dass in einem typischen GNU nicht-C locale, a/x.c++ b/x.c-- c/x.c++würde sogar in dieser Reihenfolge sortiert werden , obwohl -Art vor , +weil -, +und /‚s Primärgewicht IGNORE (so den Vergleich x.c++/a/x.c++gegen x.c--/b/x.c++vergleicht zuerst xcaxcgegen xcbxcund nur im Fall der Bindungen würde die anderen Gewichte (wobei -kommt vorher +) würde in Betracht gezogen werden.
Stéphane Chazelas
Das könnte durch den Beitritt auf , um gearbeitet /x/statt /, aber das wäre nicht der Fall befassen , in denen in der C - locale auf ASCII - basierten Systemen, a/foowürde sortiert nach a/foo.txtetwa weil /Sorten nach ..
Stéphane Chazelas
4

Da " dir1und dir2beliebige Pfadnamen sind", können wir nicht darauf zählen, dass sie aus einem einzelnen Verzeichnis (oder aus der gleichen Anzahl von Verzeichnissen) bestehen. Wir müssen also den letzten Schrägstrich in den Pfadnamen in etwas konvertieren , das an keiner anderen Stelle im Pfadnamen vorkommt. Angenommen, das Zeichen @kommt in Ihren Daten nicht vor, können Sie nach dem Basisnamen wie folgt sortieren:

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

Der erste sedBefehl ersetzt den letzten Schrägstrich in jedem Pfadnamen durch das ausgewählte Trennzeichen, der zweite kehrt die Änderung um. (Der Einfachheit halber gehe ich davon aus, dass die Pfadnamen einmal pro Zeile geliefert werden können. Wenn sie sich in einer Shell-Variablen befinden, konvertieren Sie sie zuerst in ein Format pro Zeile.)

alexis
quelle
Ha! Das ist toll! Ich habe es etwas robuster (und etwas hässlicher) gemacht, indem ich ein nicht angezeigtes Zeichen wie folgt eingegeben habe : cat pathnames | sed 's|\(.*\)/|\1'$'\4''|' | sort -t$'\4' -k+2nr | sed 's|'$'\4''|/|'. (Ich habe gerade \4vom ASCII-Tisch genommen. Anscheinend "END OF TEXT"?)
Kael
@kael, \4ist ^D(Kontrolle-D). Sofern Sie es nicht selbst am Terminal eingeben, handelt es sich um ein gewöhnliches Steuerzeichen. Mit anderen Worten, sicher auf diese Weise zu verwenden.
Alexis
3

Kurze (und etwas schnelle) Lösung: Durch Anhängen des Array-Index an die Dateinamen und Sortieren dieser können wir später eine sortierte Version basierend auf den sortierten Angaben erstellen.

Diese Lösung benötigt nur die Bash-Buildins sowie die sortBinärdatei und funktioniert auch mit allen Dateinamen, die kein Zeilenumbruchzeichen enthalten \n.

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

Für jede Datei wird der Basisname mit dem folgenden Index wiedergegeben:

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

und dann durchgeschickt sort -n.

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

Anschließend durchlaufen wir die Ausgabezeilen, extrahieren den alten Index mit der Erweiterung der Bash-Variablen ${line##* }und fügen dieses Element am Ende des neuen Arrays ein.

Nyronium
quelle
1
+1 für eine Lösung, bei der nicht der vollständige Name jeder Datei zum Sortieren übergeben werden muss
roaima
3

Dies wird sortiert, indem den Pfadnamen der Datei der Basisname vorangestellt, dieser numerisch sortiert und dann der Basisname von der Vorderseite der Zeichenfolge entfernt wird:

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

Es wäre effizienter, wenn Sie die Dateinamen in einer Liste hätten, die direkt durch eine Pipe und nicht als Shell-Array übergeben werden könnte, da die eigentliche Arbeit von der sed | sort | sedStruktur erledigt wird , aber dies reicht aus.

Diese Technik habe ich zum ersten Mal beim Codieren in Perl kennengelernt. in dieser Sprache war es als Schwartzianische Transformation bekannt .

In Bash schlägt die hier in meinem Code angegebene Transformation fehl, wenn der Basisname der Datei nicht numerisch ist. In Perl könnte es viel sicherer codiert werden.

Roaima
quelle
Vielen Dank. Was ist eine "Liste" in Bash? Unterscheidet es sich von Bash-Array? Ich habe noch nie davon gehört und es wäre großartig. Ja, das Speichern der Dateinamen in einer "Liste" könnte eine gute Idee sein. Ich habe die Dateinamen als $@oder $*von Befehlszeilenargumenten zum Ausführen eines Skripts erhalten
Tim
Das Speichern der Dateinamen in einer Datei ermöglicht externe Dienstprogramme, birgt jedoch auch das Risiko einer Fehlinterpretation von beispielsweise Zeilenumbrüchen.
Jeff Schaller
Wird Schwartzian Transform zum Sortieren einer Art Entwurfsmuster verwendet, z. B. Vorlage, Strategie, ... Muster, wie im Buch Design Pattern von Gang of Four vorgestellt?
Tim
@ JeffSchaller Zum Glück gibt es keine Zeilenumbrüche in Zahlen. Wenn ich vollständig generischen, dateinamensicheren Code schreiben würde, würde ich möglicherweise nicht bash verwenden.
Roaima
3

Für Dateinamen gleicher Tiefe.

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

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

Erläuterung

-k POS1 [, POS2] - Die empfohlene POSIX-Option zum Angeben eines Sortierfelds . Das Feld besteht aus dem Teil der Linie zwischen POS1 und POS2 (oder dem Ende der Linie, wenn POS2 weggelassen wird), einschließlich . Felder und Zeichenpositionen werden beginnend mit 1 nummeriert. Um also nach dem zweiten Feld zu sortieren, verwenden Sie "-k 2,2".

-t SEPARATOR Verwenden Sie das Zeichen SEPARATOR als Feldtrennzeichen, wenn Sie die Sortierschlüssel in jeder Zeile suchen. Standardmäßig werden Felder durch die leere Zeichenfolge zwischen einem Nicht-Leerzeichen und einem Leerzeichen getrennt.

Informationen werden vom Mann der Sorte genommen.

Der resultierende Array-Druck

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