web-dev-qa-db-fra.com

Comment trier un tableau dans Bash

J'ai un tableau dans Bash, par exemple:

array=(a c b f 3 5)

Je dois trier le tableau. Non seulement afficher le contenu de manière triée, mais aussi obtenir un nouveau tableau avec les éléments triés. Le nouveau tableau trié peut être un tout nouveau ou l'ancien.

112
B3y0nd3r

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 (tant que ce n'est pas une nouvelle ligne), et fonctionne dans Bash 3.x.

par exemple.:

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

Remarque: @sorontar a signalé vous devez faire attention si les éléments contiennent des caractères génériques tels que * ou ?:

La partie triée = ($ (...)) utilise l'opérateur "split and glob". Vous devez désactiver glob: set -f ou set -o noglob ou shopt -op noglob ou un élément du tableau tel que * sera étendu à une liste de fichiers.

Que ce passe-t-il:

Le résultat est un aboutissement de six choses qui se passent dans cet ordre:

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

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

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

Donné:

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

IFS=$'\n'définit les éléments pour que les éléments soient développés avec une nouvelle ligne en tant que délimiteur, puis créé plus tard de manière à ce que chaque ligne devient un élément. (c'est-à-dire fractionner sur une nouvelle ligne.)

La délimitation par une nouvelle ligne est importante car c’est ainsi que fonctionne sort (tri par ligne). Fractionner par seulement une nouvelle ligne n’est pas importante, mais elle 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 de un nouveau line , et serait impropre à notre exploitation.

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

<<<, appelé here strings , ==, prend le développement de "${array[*]}", comme expliqué ci-dessus, et l'insère dans l'entrée standard de sort.

Dans notre exemple, sort reçoit la chaîne suivante:

a c
b
f
3 5

Puisque sort sort , il produit:

3 5
a c
b
f

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

La partie $(...), appelée commande ) ==, provoque l’exécution de son contenu (sort <<<"${array[*]}) en tant que commande normale, en même temps que sortie standard comme littéral qui va partout où $(...) était.

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

sorted=(3 5
a c
b
f
)

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

Enfin, le unset IFS

Ceci réinitialise la valeur de IFS à la valeur par défaut et constitue simplement une bonne pratique.

Cela garantit que nous ne causons pas de problèmes avec quoi que ce soit qui repose sur IFS plus tard dans notre script. (Sinon, nous aurions besoin de nous rappeler que nous avons changé les choses - ce qui pourrait ne pas être pratique pour des scripts complexes.)

171
antak

Réponse originale:

array=(a c b "f f" 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
f f

Remarque cette version gère les valeurs contenant des caractères spéciaux ou des espaces (sauf nouvelles lignes)

Note readarray est supporté dans bash 4+. 


Edit Sur la suggestion de @ Dimitre, je l’avais mis à jour pour:

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

ce qui a l'avantage de comprendre même les éléments sorting avec des caractères de nouvelle ligne correctement intégrés. Malheureusement, comme correctement indiqué par @ruakh, cela ne signifie pas que le résultat de readarray serait correct, car readarray n'a pas la possibilité d'utiliser NUL au lieu de newlines comme séparateur de ligne.

33
sehe

Voici une implémentation pure Bash quicksort:

#!/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=(a c b f 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… alors voici un tri rapide:

#!/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 modifier 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 d'un fichier, etc. Il suffit d'utiliser le test approprié; vous pouvez même le rendre plus générique et lui demander d'utiliser un premier argument, à savoir l'utilisation de la fonction de test, 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

pour que les fichiers du dossier actuel soient triés par date de modification (la plus récente en premier).

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

32
gniourf_gniourf

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

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

Avec bash vous aurez de toute façon besoin d’un programme de tri externe.

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

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

ksh a set -s pour trier ASCIIbétiquement .

26
Dimitre Radoulov

Lors du voyage en train de 3 heures entre Munich et Francfort (que j'avais du mal à atteindre parce que la fête de la bière commence demain), je pensais à mon premier poste. Utiliser un tableau global est une bien meilleure idée pour une fonction de tri générale. La fonction suivante gère les chaînes 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 a c b 'z y' 3 5
echo ${BSORT[@]}

Cela imprime:

3 5 a b c z y

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

BSORT=(a c b 'z y' 3 5) 
bubble_sort
echo ${BSORT[@]}

Notez que Bash utilise probablement en interne des pointeurs intelligents, de sorte que l'opération-swap pourrait soit bon marché (même si j'en doute). Cependant, bubble_sort montre que des fonctions plus avancées telles que merge_sort sont également à la portée du langage Shell. 

8
Andreas Spindler

Une autre solution qui utilise la variable sort externe et gère les caractères any spéciaux (sauf pour NUL :)). Devrait fonctionner avec bash-3.2 et GNU ou BSD sort (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 des entrées à la fin. Nous utilisons printf intégré pour écrire les éléments du tableau, terminés à zéro. La citation permet de s'assurer que les éléments du tableau sont passés tels quels et que les spécificités de Shell printf lui permettent de réutiliser la dernière partie de la chaîne de format pour chaque paramètre restant. C'est-à-dire que cela équivaut à quelque chose comme:

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

La liste des éléments terminés par null est ensuite transmise à sort. L'option -z lui permet de lire, de trier et de produire les éléments terminés par un caractère nul. Si vous ne souhaitez obtenir que les éléments uniques, vous pouvez transmettre -u car il est plus portable que uniq -z. Le LC_ALL=C garantit un ordre de tri stable indépendamment des paramètres régionaux, ce qui est parfois utile pour les scripts. Si vous voulez que sort respecte les paramètres régionaux, supprimez-le.

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

Maintenant, revenons au début. La variable intégrée read lit la sortie du stdin redirigé. La définition de IFS vide désactive le fractionnement de Word, ce qui est inutile ici. Par conséquent, read lit l'intégralité de la 'ligne' de l'entrée dans la seule variable fournie. L'option -r désactive également le traitement d'échappement non souhaité. Enfin, -d '' définit le délimiteur de ligne sur NUL, c'est-à-dire qu'il indique à read de lire des chaînes terminées par un zéro.

En conséquence, la boucle est exécutée une fois pour chaque élément de tableau successif à terminaison 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 qu’un des nombreux moyens d’atteindre le même objectif. À mon avis, il est plus simple d'implémenter l'algorithme de tri complet dans bash et, dans certains cas, cela 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 :).

5
Michał Górny

essaye ça:

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

La sortie sera:

 3 
 5 
 A 
 B 
 C 
 F 

Problème résolu.

2
rsingh

tri min: 

#!/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
2
MathQues
array=(a c b f 3 5)
new_array=($(echo "${array[@]}" | sed 's/ /\n/g' | sort))    
echo ${new_array[@]}

le contenu d'écho de new_array sera:

3 5 a b c f
1
blp

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

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 -> $?"

vous pouvez ensuite utiliser ces entiers en tant qu'index de tableau, car Bash utilise toujours un tableau fragmenté, vous n'avez donc pas à vous soucier des index non utilisés

array=(a c b f 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 est parfois impossible de mapper le contenu sur des entiers uniques 32 bits.
1
Xiè Jìléi

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

Utilisez un caractère qui ne figure pas dans le tableau d'origine (tel que $'\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 trier le tableau:

$ array=( a b 'c d' $'e\nf' $'g\1h')
$ sortarray "${array[@]}"
$ printf '<%s>\n' "${sorted[@]}"
<a>
<b>
<c d>
<e
f>
<gh>

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

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

la description

  • Nous définissons deux variables locales wa (char de contournement) et un IFS nul
  • Ensuite (avec ifs null), nous testons le tableau entier $*.
  • Ne contient pas de caractère de contournement [[ $* =~ [$wa] ]].
  • Si tel est le cas, déclenchez un message et signalez une erreur: exit 1
  • Évitez les extensions de nom de fichier: set -f
  • Définissez une nouvelle valeur 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 car "${@//$nl/$wa}".
  • envoyer ces valeurs à trier sort -n.
  • et replacer toutes les valeurs triées dans les arguments de position set --.
  • Ensuite, nous affectons chaque argument un par un (pour préserver les nouvelles lignes).
  • dans une boucle for x
  • vers un nouveau tableau: sorted+=(…)
  • entre guillemets pour préserver les sauts de ligne existants.
  • restauration de la solution de contournement sur une nouvelle ligne "${x//$wa/$nl}".
  • terminé
0
sorontar

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

Voici ma mise en œuvre pour l'algorithme de tri par bulle simple.

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=(a c b f 3 5)
echo " input: ${array[@]}"
echo "output: $(bubble_sort ${array[@]})"

Cela doit imprimer:

 input: a c b f 3 5
output: 3 5 a b c f
0
Andreas Spindler
a=(e b 'c d')
shuf -e "${a[@]}" | sort >/tmp/f
mapfile -t g </tmp/f
0
Steven Penny