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.
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
ouset -o noglob
oushopt -op noglob
ou un élément du tableau tel que*
sera étendu à une liste de fichiers.
Le résultat est un aboutissement de six choses qui se passent dans cet ordre:
IFS=$'\n'
"${array[*]}"
<<<
sort
sorted=($(...))
unset IFS
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.
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
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.
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.)
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.
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.).
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 .
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.
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 :).
essaye ça:
echo ${array[@]} | awk 'BEGIN{RS=" ";} {print $1}' | sort
La sortie sera:
3 5 A B C F
Problème résolu.
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
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
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[@]}"
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
wa
(char de contournement) et un IFS nul$*
.[[ $* =~ [$wa] ]]
.exit 1
set -f
IFS=$'\n'
), une variable de boucle x
et une nouvelle ligne var (nl=$'\n'
).$@
)."${@//$nl/$wa}"
.sort -n
.set --
.for x
sorted+=(…)
"${x//$wa/$nl}"
.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
a=(e b 'c d')
shuf -e "${a[@]}" | sort >/tmp/f
mapfile -t g </tmp/f