Trier un tableau de chemins d'accès aux fichiers par leurs noms de base

8

Supposons que j'ai la liste des chemins d'accès des fichiers stockés dans un tableau

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

Je veux trier les éléments du tableau en fonction des noms de base des noms de fichiers, dans l'ordre numérique

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

Comment puis je faire ça?

Je peux seulement trier leurs parties de nom de base:

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

Je pense à

  • la création d'un tableau associatif dont les clés sont les noms de base et les valeurs sont les noms de chemin, donc l'accès aux chemins se fait toujours via les noms de base.
  • créer un autre tableau pour les noms de base uniquement et appliquer sortau tableau de noms de base.

Merci.

Tim
la source
1
Ce n'est pas une bonne idée, mais vous pouvez trier en bash
Jeff Schaller
Attention avec un tableau tapé sur les noms de base, si vous pouviez avoir dir1 / 42.pdf et dir2 / 42.pdf
Jeff Schaller
Cela (différents chemins d'accès avec le même nom de base) ne se produit pas dans mon cas. Mais si un script bash peut y faire face, ce sera parfait. Je n'ai pas de bonnes exigences sur la façon de trier les chemins d'accès avec le même nom de base, peut-être que quelqu'un d'autre peut le faire. dir1 dir2sont juste constitués, et ce sont en fait des chemins d'accès arbitraires.
Tim

Réponses:

4

Contrairement à ksh ou zsh, bash n'a pas de support intégré pour le tri des tableaux ou des listes de chaînes arbitraires. Il peut trier des globes ou la sortie de aliasou setou typeset(bien que ces 3 derniers ne soient pas dans l'ordre de tri des paramètres régionaux de l'utilisateur), mais cela ne peut pas être utilisé pratiquement ici.

Il n'y a rien dans le POSIX toolchest qui peut facilement trier des listes arbitraires de chaînes non plus¹ ( sorttrie les lignes, donc seules les séquences courtes (LINE_MAX étant souvent plus courtes que PATH_MAX) de caractères autres que NUL et retour à la ligne, tandis que les chemins de fichiers sont des séquences d'octets non vides autres que 0).

Ainsi, bien que vous puissiez implémenter votre propre algorithme de tri dans awk(en utilisant l' <opérateur de comparaison de chaînes) ou mêmebash (en utilisant [[ < ]]), pour des chemins arbitraires dans bash, de manière portable, le plus simple peut être de recourir à perl:

Avec bash4.4+, vous pourriez faire:

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

Cela donne un strcmp()ordre semblable à. Pour un ordre basé sur les règles de classement des paramètres régionaux comme dans globs ou la sortie de ls, ajoutez un -Mlocaleargument à perl. Pour le tri numérique (plus comme GNU sort -gcar il prend en charge des nombres comme +3, 1.2e-5et non pas des milliers de séparateurs, mais pas des hexadimaux), utilisez à la <=>place de cmp(et encore -Mlocalepour que la décimale de l'utilisateur soit respectée comme pour la sortcommande).

Vous serez limité par la taille maximale des arguments d'une commande. Pour éviter cela, vous pouvez passer la liste des fichiers perlsur son stdin au lieu d'arguments via:

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

Avec les anciennes versions de bash, vous pourriez utiliser une while IFS= read -rd ''boucle à la place de readarray -d ''ou obtenir la perlsortie de la liste des chemins correctement cités afin de pouvoir la transmettre eval "array=($(perl...))".

Avec zsh, vous pouvez simuler une expansion globale pour laquelle vous pouvez définir un ordre de tri:

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

Avec, reply=($filearray)nous forçons en fait l'expansion de glob (qui était initialement juste /) pour être les éléments du tableau. Ensuite, nous définissons l'ordre de tri en fonction de la queue du nom de fichier.

Pour un strcmp()ordre similaire, fixez les paramètres régionaux à C. Pour le tri numérique (similaire à GNU sort -V, pas sort -nqui fait une différence significative lors de la comparaison 1.4et 1.23(dans les paramètres régionaux où se .trouve la marque décimale) par exemple), ajoutez le nqualificatif glob.

Au lieu de oe{expression}, vous pouvez également utiliser une fonction pour définir un ordre de tri comme:

by_tail() REPLY=$REPLY:t

ou plus avancés comme:

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

(donc a/foo2bar3.pdf(2,3 nombres) trie après b/bar1foo3.pdf(1,3) mais avant c/baz2zzz10.pdf(2,10)) et utilise comme:

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

Bien sûr, ceux-ci peuvent être appliqués sur de vrais globes car c'est à cela qu'ils sont principalement destinés. Par exemple, pour une liste de pdffichiers dans n'importe quel répertoire, triés par nom de base / queue:

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

¹ Si un strcmp()tri basé sur est acceptable, et pour les chaînes courtes, vous pouvez transformer les chaînes en leur codage hexadécimal avec awkavant de passer à sortet reconvertir après le tri.

Stéphane Chazelas
la source
Voir cette réponse ci-dessous pour un excellent bash one-liner: unix.stackexchange.com/a/394166/41735
kael
9

sortdans GNU coreutils permet un séparateur et une clé de champ personnalisés. Vous définissez /comme séparateur de champ et triez en fonction du deuxième champ pour trier sur le nom de base, au lieu du chemin entier.

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

dir2/0003.pdf
dir1/0010.pdf
dir3/0040.pdf
Gowtham
la source
4
C'est une option standard pour sort, pas une extension GNU. Cela fonctionnera si les chemins sont tous de la même longueur.
Kusalananda
Même réponse en même temps :)
MiniMax
2
Cela ne fonctionne que si les chemins contiennent chacun un seul répertoire. Et alors some/long/path/0011.pdf? Pour autant que je puisse voir sur sa page de manuel, sortne contient aucune option pour trier par le dernier champ.
Federico Poloni
5

Tri par gawk expression (soutenu par bash « s readarray):

Exemple de tableau de noms de fichiers contenant des espaces blancs :

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

Le résultat:

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

Accès à un seul élément:

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

Cela suppose qu'aucun chemin de fichier ne contient de caractères de nouvelle ligne. Notez que le tri numérique des valeurs dans @val_num_ascs'applique uniquement à la partie numérique principale de la clé (aucune dans cet exemple) avec retour à la comparaison lexicale (basée sur strcmp(), et non l'ordre de tri des paramètres régionaux) pour les liens.

RomanPerekhrest
la source
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

Le tri des noms de fichiers avec des retours à la ligne dans leurs noms entraînera des problèmes à l' sortétape.

Il génère une /liste délimitée avec awkqui contient le nom de base dans la première colonne et le chemin complet comme les colonnes restantes:

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

C'est ce qui est trié et cutest utilisé pour supprimer la première /colonne délimitée. Le résultat est transformé en un nouveau bashtableau.

Kusalananda
la source
@ StéphaneChazelas Un peu poilu, mais ok ...
Kusalananda
Notez que sans doute, il calcule le mauvais nom de base pour des chemins comme /some/dir/.
Stéphane Chazelas
@ StéphaneChazelas Oui, mais l'OP a spécifiquement dit qu'il avait des chemins de fichiers, donc je suppose simplement qu'il y a un nom de base correct à la fin du chemin.
Kusalananda
Notez que dans un environnement local GNU non-C typique, a/x.c++ b/x.c-- c/x.c++serait trié dans cet ordre même si trié -avant +parce que -, +et /le poids principal de est IGNORE (donc la comparaison x.c++/a/x.c++avec le x.c--/b/x.c++premier se compare xcaxcau xcbxc, et seulement en cas de liens les autres poids (où -avant +) serait pris en compte.
Stéphane Chazelas
Cela pourrait être contourné en se joignant à la /x/place de /, mais cela ne résoudrait pas le cas où, dans l'environnement local C sur les systèmes basés sur ASCII, a/footrierait après, a/foo.txtpar exemple, car /trie après ..
Stéphane Chazelas
4

Puisque " dir1et dir2sont des chemins d'accès arbitraires", nous ne pouvons pas compter sur eux comme un seul répertoire (ou le même nombre de répertoires). Nous devons donc convertir la dernière barre oblique dans les chemins d'accès en quelque chose qui ne se produit pas ailleurs dans le chemin d'accès. En supposant que le caractère @ne se produit pas dans vos données, vous pouvez trier par nom de base comme ceci:

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

La première sedcommande remplace la dernière barre oblique de chaque chemin par le séparateur choisi, la seconde inverse la modification. (Par souci de simplicité, je suppose que les chemins d'accès peuvent être fournis un par ligne. S'ils sont dans une variable shell, convertissez-les d'abord au format un par ligne.)

alexis
la source
Ha! C'est bien! Je l' ai fait un peu plus robuste (et un peu plus laid) par substratum un caractère non-affichage comme ceci: cat pathnames | sed 's|\(.*\)/|\1'$'\4''|' | sort -t$'\4' -k+2nr | sed 's|'$'\4''|/|'. (Je viens de saisir \4de la table ascii. Apparemment "FIN DU TEXTE"?)
kael
@kael, \4est ^D(contrôle-D). À moins que vous ne le tapiez vous-même au terminal, il s'agit d'un caractère de contrôle ordinaire. En d'autres termes, sûr à utiliser de cette façon.
alexis
3

Solution courte (et quelque peu rapide): en ajoutant l'index du tableau aux noms de fichiers et en les triant, nous pouvons plus tard créer une version triée en fonction des indices triés.

Cette solution n'a besoin que des fonctions internes bash ainsi que du sortbinaire, et fonctionne également avec tous les noms de fichiers qui n'incluent pas de \ncaractère de nouvelle ligne .

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

Pour chaque fichier, nous faisons écho à son nom de base avec son index initial ajouté comme ceci:

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

puis envoyé sort -n.

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

Ensuite, nous parcourons les lignes de sortie, extrayons l'ancien index avec l'expansion de la variable bash ${line##* }et insérons cet élément à la fin du nouveau tableau.

nyronium
la source
1
+1 pour une solution qui ne nécessite pas de passer le nom complet de chaque fichier pour trier
roaima
3

Cela trie en ajoutant les chemins d'accès aux fichiers avec le nom de base, en les triant numériquement, puis en supprimant le nom de base du début de la chaîne:

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

Ce serait plus efficace si vous aviez les noms de fichiers dans une liste qui pourrait être passée directement via un canal plutôt que comme un tableau de shell, car le travail réel est effectué par la sed | sort | sedstructure, mais cela suffit.

J'ai découvert cette technique lors du codage en Perl; dans cette langue, il était connu comme une transformation schwartzienne .

Dans Bash, la transformation indiquée ici dans mon code échouera si vous avez des non-numériques dans le nom de base du fichier. En Perl, il pourrait être codé de manière beaucoup plus sûre.

roaima
la source
Merci. qu'est-ce qu'une "liste" dans bash? Est-ce différent du tableau bash? Je n'en ai jamais entendu parler et ce serait formidable. oui, le stockage des noms de fichiers dans une "liste" pourrait être une bonne idée. J'ai obtenu les noms de fichiers sous la forme $@ou à $*partir d'arguments de ligne de commande pour exécuter un script
Tim
Le stockage des noms de fichier dans un fichier permet des utilitaires externes, mais risque également une mauvaise interprétation, par exemple, des retours à la ligne.
Jeff Schaller
La transformation de Schwartzian est-elle utilisée pour trier une sorte de modèle de conception, par exemple un modèle, une stratégie, ... des modèles, comme présenté dans le livre Design Pattern by Gang of Four?
Tim
@JeffSchaller heureusement, il n'y a pas de nouvelle ligne dans les chiffres. Si j'écrivais un code entièrement générique sûr pour les noms de fichiers, je n'utiliserais probablement pas bash.
roaima
3

Pour des noms de fichiers de même profondeur.

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

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

Explication

-k POS1 [, POS2] - L'option recommandée, POSIX, pour spécifier un champ de tri. Le champ se compose de la partie de la ligne entre POS1 et POS2 (ou la fin de la ligne, si POS2 est omis), inclusivement . Les champs et les positions des caractères sont numérotés en commençant par 1. Donc, pour trier sur le deuxième champ, vous utiliseriez `-k 2,2 '.

-t SEPARATOR Utilisez le caractère SEPARATOR comme séparateur de champ lors de la recherche des clés de tri dans chaque ligne. Par défaut, les champs sont séparés par la chaîne vide entre un caractère non blanc et un caractère blanc.

Les informations proviennent de l'homme du tri.

L'impression de la matrice résultante

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