Comment sortinger un tableau dans Bash

J’ai un tableau dans Bash, par exemple:

array=(acbf 3 5) 

J’ai besoin de sortinger le tableau. Non seulement afficher le contenu de manière sortingée, mais pour obtenir un nouveau tableau avec les éléments sortingés. Le nouveau tableau sortingé peut être complètement nouveau ou ancien.

Vous n’avez pas vraiment besoin de tout ce code:

 IFS=$'\n' sorted=($(sort < <<"${array[*]}")) unset IFS 

Prend en charge les espaces dans les éléments (à condition que ce ne soit pas une nouvelle ligne) et fonctionne dans Bash 3.x.

par exemple:

 $ array=("ac" bf "3 5") $ IFS=$'\n' sorted=($(sort < <<"${array[*]}")) $ printf "[%s]\n" "${sorted[@]}" [3 5] [ac] [b] [f] 

Remarque: @sorontar a souligné que des précautions sont nécessaires si des éléments contiennent des caractères génériques tels que * ou ? :

La partie named = ($ (...)) utilise l’opérateur "split and glob". Vous devez activer glob off: set -f ou set -o noglob ou shopt -op noglob ou un élément du tableau comme * sera étendu à une liste de fichiers.

Que ce passe-t-il:

Le résultat est un point culminant six choses qui se produisent dans cet ordre:

  1. IFS=$'\n'
  2. "${array[*]}"
  3. < <<
  4. sort
  5. sorted=($(...))
  6. unset IFS

Tout d'abord, l' IFS=$'\n'

Ceci est une partie importante de notre opération qui affecte les résultats de 2 et 5 de la manière suivante:

Donné:

  • "${array[*]}" développe sur chaque élément délimité par le premier caractère de IFS
  • sorted=() crée des éléments en divisant chaque caractère de IFS

IFS=$'\n' met les choses en place pour que les éléments soient développés en utilisant une nouvelle ligne comme délimiteur, puis créés plus tard pour que chaque ligne devienne un élément. (c'est-à-dire diviser sur une nouvelle ligne.)

La délimitation par une nouvelle ligne est importante car c'est ainsi que le sort fonctionne (sorting par ligne). Diviser par une nouvelle ligne n'est pas aussi important, mais est nécessaire pour préserver les éléments contenant des espaces ou des tabulations.

La valeur par défaut de IFS est un espace , un onglet , suivi d' une nouvelle ligne , et serait impropre à notre opération.

Ensuite, le sort < <<"${array[*]}" partie

< << , appelé ici ssortingngs , prend l'expansion de "${array[*]}" , comme expliqué ci-dessus, et l'introduit dans l'entrée standard de sort .

Avec notre exemple, sort est alimenté par la chaîne suivante:

 ac b f 3 5 

Depuis les sortes de sort , il produit:

 3 5 ac b f 

Ensuite, la partie sorted=($(...))

La partie $(...) , appelée substitution de commande , fait fonctionner son contenu ( sort < <<"${array[*]} ) comme une commande normale, tout en prenant la sortie standard résultante comme le littéral $(...) était.

Dans notre exemple, cela produit quelque chose de similaire à la simple écriture:

 sorted=(3 5 ac b f ) 

sorted devient alors un tableau créé en divisant ce littéral sur chaque nouvelle ligne.

Enfin, le unset IFS

Cela réinitialise la valeur d' IFS à la valeur par défaut et constitue une bonne pratique.

C'est pour s'assurer que nous ne causons pas de problèmes avec tout ce qui repose sur IFS plus tard dans notre script. (Sinon, nous devrions nous rappeler que nous avons changé les choses - ce qui pourrait ne pas être pratique pour les scripts complexes.)

Réponse originale:

 array=(acb "ff" 3 5) readarray -t sorted < <(for a in "${array[@]}"; do echo "$a"; done | sort) 

sortie:

 $ for a in "${sorted[@]}"; do echo "$a"; done 3 5 a b c ff 

Notez que cette version fait face aux valeurs qui contiennent des caractères spéciaux ou des espaces ( sauf les nouvelles lignes)

Remarque readarray est pris en charge dans bash 4+.


Modifier Sur la base de la suggestion de @Dimitre, je l'ai mise à jour pour:

 readarray -t sorted < <(printf '%s\0' "${array[@]}" | sort -z | xargs -0n1) 

qui a l'avantage de comprendre même les éléments de sorting avec des caractères de nouvelle ligne intégrés correctement. Malheureusement, comme cela a été signalé correctement par @ruakh, cela ne signifie pas que le résultat de readarray serait correct , car readarray n’a pas d’option pour utiliser NUL au lieu de lignes régulières comme séparateurs de lignes.

Voici une implémentation rapide de Bash:

 #!/bin/bash # quicksorts positional arguments # return is in array qsort_ret qsort() { local pivot i smaller=() larger=() qsort_ret=() (($#==0)) && return 0 pivot=$1 shift for i; do if [[ $i < $pivot ]]; then smaller+=( "$i" ) else larger+=( "$i" ) fi done qsort "${smaller[@]}" smaller=( "${qsort_ret[@]}" ) qsort "${larger[@]}" larger=( "${qsort_ret[@]}" ) qsort_ret=( "${smaller[@]}" "$pivot" "${larger[@]}" ) } 

Utiliser comme, par exemple,

 $ array=(acbf 3 5) $ qsort "${array[@]}" $ declare -p qsort_ret declare -a qsort_ret='([0]="3" [1]="5" [2]="a" [3]="b" [4]="c" [5]="f")' 

Cette implémentation est récursive… voici donc un quicksort itératif:

 #!/bin/bash # quicksorts positional arguments # return is in array qsort_ret # Note: iterative, NOT recursive! :) qsort() { (($#==0)) && return 0 local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger qsort_ret=("$@") while ((${#stack[@]})); do beg=${stack[0]} end=${stack[1]} stack=( "${stack[@]:2}" ) smaller=() larger=() pivot=${qsort_ret[beg]} for ((i=beg+1;i< =end;++i)); do if [[ "${qsort_ret[i]}" < "$pivot" ]]; then smaller+=( "${qsort_ret[i]}" ) else larger+=( "${qsort_ret[i]}" ) fi done qsort_ret=( "${qsort_ret[@]:0:beg}" "${smaller[@]}" "$pivot" "${larger[@]}" "${qsort_ret[@]:end+1}" ) if ((${#smaller[@]}>=2)); then stack+=( "$beg" "$((beg+${#smaller[@]}-1))" ); fi if ((${#larger[@]}>=2)); then stack+=( "$((end-${#larger[@]}+1))" "$end" ); fi done } 

Dans les deux cas, vous pouvez changer l'ordre que vous utilisez: j'ai utilisé des comparaisons de chaînes, mais vous pouvez utiliser des comparaisons arithmétiques, comparer le temps de modification du fichier, etc. vous pouvez même le rendre plus générique et utiliser un premier argument qui est la fonction de test utilisée, par exemple,

 #!/bin/bash # quicksorts positional arguments # return is in array qsort_ret # Note: iterative, NOT recursive! :) # First argument is a function name that takes two arguments and compares them qsort() { (($#< =1)) && return 0 local compare_fun=$1 shift local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger qsort_ret=("$@") while ((${#stack[@]})); do beg=${stack[0]} end=${stack[1]} stack=( "${stack[@]:2}" ) smaller=() larger=() pivot=${qsort_ret[beg]} for ((i=beg+1;i<=end;++i)); do if "$compare_fun" "${qsort_ret[i]}" "$pivot"; then smaller+=( "${qsort_ret[i]}" ) else larger+=( "${qsort_ret[i]}" ) fi done qsort_ret=( "${qsort_ret[@]:0:beg}" "${smaller[@]}" "$pivot" "${larger[@]}" "${qsort_ret[@]:end+1}" ) if ((${#smaller[@]}>=2)); then stack+=( "$beg" "$((beg+${#smaller[@]}-1))" ); fi if ((${#larger[@]}>=2)); then stack+=( "$((end-${#larger[@]}+1))" "$end" ); fi done } 

Ensuite, vous pouvez avoir cette fonction de comparaison:

 compare_mtime() { [[ $1 -nt $2 ]]; } 

et utilise:

 $ qsort compare_mtime * $ declare -p qsort_ret 

avoir les fichiers dans le dossier actuel sortingés par heure de modification (le plus récent en premier).

REMARQUE. Ces fonctions sont purement Bash! pas d'utilitaires externes et pas de sous-couches! ils sont sans danger avec tous les symboles amusants que vous pouvez avoir (espaces, caractères de nouvelle ligne, caractères globaux, etc.).

Si vous n’avez pas besoin de gérer des caractères de shell spéciaux dans les éléments du tableau:

 array=(acbf 3 5) sorted=($(printf '%s\n' "${array[@]}"|sort)) 

Avec bash, vous aurez besoin d’un programme de sorting externe.

Avec zsh, aucun programme externe n’est nécessaire et les caractères de shell spéciaux sont facilement gérés:

 % array=('aa' cbf 3 5); printf '%s\n' "${(o)array[@]}" 3 5 aa b c f 

ksh a set -s pour sortinger les ASCIIbétiquement .

Lors du voyage en train de 3 heures de Munich à Francfort (que j’ai eu du mal à atteindre car l’Oktoberfest commence demain), je pensais à mon premier message. Utiliser un tableau global est une bien meilleure idée pour une fonction de sorting générale. La fonction suivante gère les chaînes de caractères arbitraires (nouvelles lignes, blancs, etc.):

 declare BSORT=() function bubble_sort() { # # @param [ARGUMENTS]... # # Sort all positional arguments and store them in global array BSORT. # Without arguments sort this array. Return the number of iterations made. # # Bubble sorting lets the heaviest element sink to the bottom. # (($# > 0)) && BSORT=("$@") local j=0 ubound=$((${#BSORT[*]} - 1)) while ((ubound > 0)) do local i=0 while ((i < ubound)) do if [ "${BSORT[$i]}" \> "${BSORT[$((i + 1))]}" ] then local t="${BSORT[$i]}" BSORT[$i]="${BSORT[$((i + 1))]}" BSORT[$((i + 1))]="$t" fi ((++i)) done ((++j)) ((--ubound)) done echo $j } bubble_sort acb 'zy' 3 5 echo ${BSORT[@]} 

Cela imprime:

 3 5 abczy 

La même sortie est créée à partir de

 BSORT=(acb 'zy' 3 5) bubble_sort echo ${BSORT[@]} 

Notez que Bash utilise probablement des pointeurs intelligents en interne, de sorte que l’opération d’échange pourrait être bon marché (bien que j’en doute). Cependant, bubble_sort démontre que des fonctions plus avancées telles que merge_sort sont également à la scope du langage shell.

tl; dr :

Triez le tableau a_in et stockez le résultat dans a_out (les éléments ne doivent pas avoir de nouvelles lignes intégrées [1] ):

Bash v4 +:

 readarray -t a_out < <(printf '%s\n' "${a_in[@]}" | sort) 

Bash v3:

 IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "${a_in[@]}" | sort) 

Avantages par rapport à la solution antak :

  • Vous n'avez pas besoin de vous soucier des globalisations accidentelles (interprétation accidentelle des éléments du tableau en tant que modèles de nom de fichier), aucune commande supplémentaire n'est donc nécessaire pour désactiver le globbing ( set -f et set +f pour le restaurer ultérieurement).

  • Vous n'avez pas besoin de vous soucier de réinitialiser IFS avec unset IFS . [2]


Lecture facultative: explication et exemple de code

Ce qui précède combine le code Bash avec le sort utilitaires externes pour une solution qui fonctionne avec des éléments à une seule ligne arbitraires et un sorting lexical ou numérique (éventuellement par champ) :

  • Performance : pour environ 20 éléments ou plus , cela sera plus rapide qu'une solution Bash pure - de manière significative et croissante dès lors que vous aurez dépassé la centaine d'éléments.
    (Les seuils exacts dépendent de votre entrée, de votre machine et de votre plate-forme spécifiques.)

    • La raison en est que cela évite les boucles Bash .
  • printf '%s\n' "${a_in[@]}" | sort printf '%s\n' "${a_in[@]}" | sort effectue le sorting (lexicalement, par défaut - voir la spécification POSIX du sort ):

    • "${a_in[@]}" toute sécurité vers les éléments du tableau a_in tant qu'arguments individuels , quels qu'ils soient (y compris les espaces).

    • printf '%s\n' imprime ensuite chaque argument - c'est-à-dire chaque élément du tableau - sur sa propre ligne, tel quel.

  • Notez l' utilisation d'une substitution de processus ( < (...) ) pour fournir la sortie sortingée en entrée à read / readarray (via la redirection vers stdin, < ), car read / readarray doit s'exécuter dans le shell actuel (ne doit pas être exécuté dans un sous - shell ) pour que la variable de sortie a_out soit visible pour le shell actuel (pour que la variable rest définie dans le rest du script).

  • Lecture de la sortie du sort dans une variable de tableau :

    • Bash v4 +: readarray -t a_out lit les lignes individuelles sorties par sort dans les éléments de la variable tableau a_out , sans inclure le \n a_out dans chaque élément ( -t ).

    • Bash v3: readarray n'existe pas, donc read doit être utilisé:
      IFS=$'\n' read -d '' -r -a a_out indique à read de lire dans le tableau ( -a ) la variable a_out , en lisant l'intégralité de l'entrée, entre les lignes ( -d '' ), mais en le divisant en éléments de tableau par newlines ( IFS=$'\n' . $'\n' , qui produit une nouvelle ligne littérale (LF), est une chaîne appelée C ANSI ).
      ( -r , une option qui devrait toujours être utilisée avec read , désactive la gestion inattendue des caractères.)

Exemple de code annoté:

 #!/usr/bin/env bash # Define input array `a_in`: # Note the element with embedded whitespace ('a c')and the element that looks like # a glob ('*'), chosen to demonstrate that elements with line-internal whitespace # and glob-like contents are correctly preserved. a_in=( 'ac' bf 5 '*' 10 ) # Sort and store output in array `a_out` # Saving back into `a_in` is also an option. IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "${a_in[@]}" | sort) # Bash 4.x: use the simpler `readarray -t`: # readarray -t a_out < <(printf '%s\n' "${a_in[@]}" | sort) # Print sorted output array, line by line: printf '%s\n' "${a_out[@]}" 

En raison de l'utilisation du sort sans options, cela permet un sorting lexical (les chiffres sont sortingés avant les lettres et les séquences de chiffres sont traitées lexicalement, et non pas des nombres):

 * 10 5 ac b f 

Si vous vouliez un sorting numérique par le premier champ, vous utiliseriez sort -k1,1n au lieu de simplement sort , ce qui donne (les non-nombres sont sortingés avant les nombres et les nombres sont sortingés correctement):

 * ac b f 5 10 

[1] Pour manipuler des éléments avec des nouvelles lignes incorporées, utilisez la variante suivante (Bash v4 +, avec sort GNU ):
readarray -d '' -t a_out < <(printf '%s\0' "${a_in[@]}" | sort -z) .
La réponse utile de Michał Górny est une solution Bash v3.

[2] Alors que IFS est défini dans la variante Bash v3, la modification est étendue à la commande .
En revanche, ce qui suit IFS=$'\n' dans la réponse d'Antak est une assignation plutôt qu'une commande, auquel cas le changement IFS est global .

Une autre solution qui utilise un sort externe et permet de gérer tous les caractères spéciaux (sauf pour les NUL :)). Devrait fonctionner avec bash-3.2 et le sort GNU ou BSD (malheureusement, POSIX n’inclut pas -z ).

 local e new_array=() while IFS= read -r -d '' e; do new_array+=( "${e}" ) done < <(printf "%s\0" "${array[@]}" | LC_ALL=C sort -z) 

Regardez d'abord la redirection d'entrée à la fin. Nous utilisons printf intégré pour écrire les éléments du tableau, terminés à zéro. Les guillemets vérifient que les éléments du tableau sont transmis tels quels, et que les spécificités du shell printf entraînent la réutilisation de la dernière partie de la chaîne de format pour chaque paramètre restant. Autrement dit, cela équivaut à quelque chose comme:

 for e in "${array[@]}"; do printf "%s\0" "${e}" done 

La liste d'éléments à terminaison nulle est ensuite passée à sort . L'option -z permet de lire les éléments terminés par un caractère nul, de les sortinger et de générer également un résultat nul. Si vous n'aviez besoin que des éléments uniques, vous pouvez passer -u car il est plus portable que uniq -z . Le LC_ALL=C garantit un ordre de sorting stable indépendamment des parameters régionaux - parfois utile pour les scripts. Si vous souhaitez que le sort respecte les parameters régionaux, supprimez-le.

La construction < () obtient le descripteur à lire dans le pipeline généré, et < redirige l'entrée standard de la boucle while vers celle-ci. Si vous avez besoin d'accéder à l'entrée standard dans le tube, vous pouvez utiliser un autre descripteur - exercice pour le lecteur :).

Maintenant, revenons au début. La read intégrée lit la sortie du stdin redirigé. Le fait de définir des IFS vides désactive le fractionnement de mots inutile ici. En conséquence, read lit la totalité de la ligne d'entrée dans la variable unique fournie. -r option -r désactive également le traitement d'échappement indésirable ici. Enfin, -d '' définit le délimiteur de ligne sur NUL - c’est-à-dire, indique à read de lire les chaînes terminées à zéro.

Par conséquent, la boucle est exécutée une fois pour chaque élément de tableau terminé par zéro, la valeur étant stockée dans e . L'exemple met simplement les éléments dans un autre tableau, mais vous préférerez peut-être les traiter directement :).

Bien sûr, ce n’est que l’un des nombreux moyens d’atteindre le même objective. À mon avis, il est plus simple d’implémenter un algorithme de sorting complet dans bash et dans certains cas, il sera plus rapide. Il gère tous les caractères spéciaux, y compris les nouvelles lignes, et devrait fonctionner sur la plupart des systèmes courants. Plus important encore, il peut vous apprendre quelque chose de nouveau et de génial à propos de bash :).

essaye ça:

 echo ${array[@]} | awk 'BEGIN{RS=" ";} {print $1}' | sort 

La sortie sera:

 3
 5
 une
 b
 c
 F

Problème résolu.

Si vous pouvez calculer un entier unique pour chaque élément du tableau, comme ceci:

 tab='0123456789abcdefghijklmnopqrstuvwxyz' # build the reversed ordinal map for ((i = 0; i < ${#tab}; i++)); do declare -g ord_${tab:i:1}=$i done function sexy_int() { local sum=0 local i ch ref for ((i = 0; i < ${#1}; i++)); do ch="${1:i:1}" ref="ord_$ch" (( sum += ${!ref} )) done return $sum } sexy_int hello echo "hello -> $?" sexy_int world echo "world -> $?" 

alors, vous pouvez utiliser ces entiers comme index de tableau, car Bash utilise toujours un tableau fragmenté, donc pas besoin de s’inquiéter des index inutilisés:

 array=(acbf 3 5) for el in "${array[@]}"; do sexy_int "$el" sorted[$?]="$el" done echo "${sorted[@]}" 
  • Avantages. Vite.
  • Les inconvénients. Les éléments dupliqués sont fusionnés et il peut être impossible de mapper le contenu à des entiers uniques de 32 bits.
 array=(acbf 3 5) new_array=($(echo "${array[@]}" | sed 's/ /\n/g' | sort)) echo ${new_array[@]} 

le contenu echo de new_array sera:

 3 5 abcf 

min sorting:

 #!/bin/bash array=(.....) index_of_element1=0 while (( ${index_of_element1} < ${#array[@]} )); do element_1="${array[${index_of_element1}]}" index_of_element2=$((index_of_element1 + 1)) index_of_min=${index_of_element1} min_element="${element_1}" for element_2 in "${array[@]:$((index_of_element1 + 1))}"; do min_element="`printf "%s\n%s" "${min_element}" "${element_2}" | sort | head -n+1`" if [[ "${min_element}" == "${element_2}" ]]; then index_of_min=${index_of_element2} fi let index_of_element2++ done array[${index_of_element1}]="${min_element}" array[${index_of_min}]="${element_1}" let index_of_element1++ done 

Je ne suis pas convaincu que vous aurez besoin d’un programme de sorting externe dans Bash.

Voici mon implémentation pour l’algorithme simple de sorting à bulles.

 function bubble_sort() { # # Sorts all positional arguments and echoes them back. # # Bubble sorting lets the heaviest (longest) element sink to the bottom. # local array=($@) max=$(($# - 1)) while ((max > 0)) do local i=0 while ((i < max)) do if [ ${array[$i]} \> ${array[$((i + 1))]} ] then local t=${array[$i]} array[$i]=${array[$((i + 1))]} array[$((i + 1))]=$t fi ((i += 1)) done ((max -= 1)) done echo ${array[@]} } array=(acbf 3 5) echo " input: ${array[@]}" echo "output: $(bubble_sort ${array[@]})" 

Cela doit imprimer:

  input: acbf 3 5 output: 3 5 abcf 
 a=(eb 'c d') shuf -e "${a[@]}" | sort >/tmp/f mapfile -tg  

Il existe une solution de contournement pour le problème habituel des espaces et des nouvelles lignes:

Utilisez un caractère qui n’est pas dans le tableau d’origine (comme $'\1' ou $'\4' ou similaire).

Cette fonction fait le travail:

 # Sort an Array may have spaces or newlines with a workaround (wa=$'\4') sortarray(){ local wa=$'\4' IFS='' if [[ $* =~ [$wa] ]]; then echo "$0: error: array contains the workaround char" >&2 exit 1 fi set -f; local IFS=$'\n' x nl=$'\n' set -- $(printf '%s\n' "${@//$nl/$wa}" | sort -n) for x do sorted+=("${x//$wa/$nl}") done } 

Cela va sortinger le tableau:

 $ array=( ab 'cd' $'e\nf' $'g\1h') $ sortarray "${array[@]}" $ printf '< %s>\n' "${sorted[@]}"      

Cela se plaint que le tableau source contient le caractère de contournement:

 $ array=( ab 'cd' $'e\nf' $'g\4h') $ sortarray "${array[@]}" ./script: error: array contains the workaround char 

la description

  • Nous définissons deux variables locales wa (contournement char) et une valeur nulle IFS
  • Alors (avec ifs null) on teste tout le tableau $* .
  • Ne contient pas de caractère indépendant [[ $* =~ [$wa] ]] .
  • Si c’est le cas, relevez un message et signalez une erreur: exit 1
  • Évitez les extensions de nom de fichier: set -f
  • Définissez une nouvelle valeur de IFS ( IFS=$'\n' ) une variable de boucle x et une nouvelle ligne var ( nl=$'\n' ).
  • Nous imprimons toutes les valeurs des arguments reçus (le tableau d’entrée $@ ).
  • mais nous remplaçons toute nouvelle ligne par la solution de contournement "${@//$nl/$wa}" .
  • envoyer ces valeurs à sortinger sort -n .
  • et replacer toutes les valeurs sortingées dans l’ set -- arguments set -- .
  • Ensuite, nous assignons chaque argument un par un (pour préserver les nouvelles lignes).
  • en boucle for x
  • à un nouveau tableau: sorted+=(…)
  • entre guillemets pour préserver toute nouvelle ligne existante.
  • restaurer la solution de contournement sur une nouvelle ligne "${x//$wa/$nl}" .
  • terminé

sorted=($(echo ${array[@]} | tr " " "\n" | sort))

Dans l’esprit de bash / linux, je choisirais le meilleur outil de ligne de commande pour chaque étape. sort fait le job principal mais nécessite une entrée séparée par newline au lieu d’espace, donc le pipeline très simple ci-dessus fait simplement:

Contenu du tableau d’écho -> remplacer un espace par un nouveau trait -> sortinger

$() est de faire écho au résultat

($()) est de mettre le “résultat écho” dans un tableau

Note : comme @sorontar mentionné dans un commentaire à une autre question:

La partie named = ($ (…)) utilise l’opérateur “split and glob”. Vous devez activer glob off: set -f ou set -o noglob ou shopt -op noglob ou un élément du tableau comme * sera étendu à une liste de fichiers.