Comment puis-je obtenir des valeurs uniques à partir d'un tableau dans Bash?


93

J'ai presque la même question qu'ici .

J'ai un tableau qui contient aa ab aa ac aa ad, etc. Maintenant, je veux sélectionner tous les éléments uniques de ce tableau. Je pensais que ce serait simple avec sort | uniqou avec sort -ucomme ils l'ont mentionné dans cette autre question, mais rien n'a changé dans le tableau ... Le code est:

echo `echo "${ids[@]}" | sort | uniq`

Qu'est-ce que je fais mal?

Réponses:


131

Un peu piraté, mais cela devrait le faire:

echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '

Pour enregistrer les résultats uniques triés dans un tableau, effectuez une affectation de tableau :

sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))

Si votre shell prend en charge herestrings ( bashdevrait), vous pouvez épargner un echoprocessus en le modifiant en:

tr ' ' '\n' <<< "${ids[@]}" | sort -u | tr '\n' ' '

Contribution:

ids=(aa ab aa ac aa ad)

Production:

aa ab ac ad

Explication:

  • "${ids[@]}"- Syntaxe pour travailler avec des tableaux shell, qu'ils soient utilisés dans le cadre echoou dans un héritage. La @partie signifie "tous les éléments du tableau"
  • tr ' ' '\n'- Convertissez tous les espaces en nouvelles lignes. Parce que votre tableau est vu par shell comme des éléments sur une seule ligne, séparés par des espaces; et parce que sort s'attend à ce que l'entrée soit sur des lignes séparées.
  • sort -u - trier et conserver uniquement les éléments uniques
  • tr '\n' ' ' - convertissez les nouvelles lignes que nous avons ajoutées précédemment en espaces.
  • $(...)- Substitution de commande
  • À part: tr ' ' '\n' <<< "${ids[@]}"est une façon plus efficace de faire:echo "${ids[@]}" | tr ' ' '\n'

37
+1. Un peu plus ordonné: stockez les éléments uniq dans un nouveau tableau:uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}"
glenn jackman

@glennjackman oh super! Je ne savais même pas que vous pouviez utiliser de printfcette façon (donnez plus d'arguments que de chaînes de format)
sampson-chen

4
+1 Je ne suis pas sûr que ce soit un cas isolé, mais mettre des objets uniques de nouveau dans un tableau nécessaire entre parenthèses supplémentaires tels que: sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')). Sans les parenthèses supplémentaires, il le donnait sous forme de chaîne.
whla

3
Si vous ne souhaitez pas modifier l'ordre des éléments, utilisez à la ... | uniq | ...place de ... | sort -u | ....
Jesse Chisholm

2
@Jesse, uniqsupprime uniquement les doublons consécutifs . Dans l'exemple de cette réponse, sorted_unique_idsfinira par être identique à l'original ids. Pour préserver l'ordre, essayez ... | awk '!seen[$0]++'. Voir également stackoverflow.com/questions/1444406/… .
Rob Kennedy

29

Si vous exécutez Bash version 4 ou supérieure (ce qui devrait être le cas dans n'importe quelle version moderne de Linux), vous pouvez obtenir des valeurs de tableau uniques dans bash en créant un nouveau tableau associatif contenant chacune des valeurs du tableau d'origine. Quelque chose comme ça:

$ a=(aa ac aa ad "ac ad")
$ declare -A b
$ for i in "${a[@]}"; do b["$i"]=1; done
$ printf '%s\n' "${!b[@]}"
ac ad
ac
aa
ad

Cela fonctionne car dans n'importe quel tableau (associatif ou traditionnel, dans n'importe quelle langue), chaque clé ne peut apparaître qu'une seule fois. Lorsque la forboucle atteint la deuxième valeur de aain a[2], elle écrase la valeur b[aa]initialement définie pour a[0].

Faire les choses en natif bash peut être plus rapide que d'utiliser des tubes et des outils externes comme sortet uniq, bien que pour des ensembles de données plus volumineux, vous obtiendrez probablement de meilleures performances si vous utilisez un langage plus puissant comme awk, python, etc.

Si vous vous sentez confiant, vous pouvez éviter la forboucle en utilisant printfla capacité de recycler son format pour plusieurs arguments, bien que cela semble être nécessaire eval. (Arrêtez de lire maintenant si cela vous convient.)

$ eval b=( $(printf ' ["%s"]=1' "${a[@]}") )
$ declare -p b
declare -A b=(["ac ad"]="1" [ac]="1" [aa]="1" [ad]="1" )

La raison pour laquelle cette solution nécessite evalest que les valeurs de tableau sont déterminées avant la division des mots. Cela signifie que la sortie de la substitution de commande est considérée comme un mot unique plutôt que comme un ensemble de paires clé = valeur.

Bien que cela utilise un sous-shell, il utilise uniquement des composants intégrés bash pour traiter les valeurs du tableau. Assurez-vous d'évaluer votre utilisation d' evalun œil critique. Si vous n'êtes pas sûr à 100% que chepner, glenn jackman ou greycat ne trouveront aucun problème avec votre code, utilisez plutôt la boucle for.


génère une erreur: niveau de récursivité de l'expression dépassé
Benubird

1
@Benubird - pouvez-vous peut-être coller le contenu de votre terminal? Cela fonctionne parfaitement pour moi, donc ma meilleure supposition est que vous avez (1) une faute de frappe, (2) une ancienne version de bash (des tableaux associatifs ont été ajoutés à la v4), ou (3) un afflux ridiculement important d'arrière-plan cosmique le rayonnement causé par le trou noir quantique dans le sous-sol de votre voisin, générant des interférences avec les signaux de votre ordinateur.
ghoti

1
ne peut pas, n'a pas gardé celui qui n'a pas fonctionné mais, j'ai essayé d'exécuter le vôtre tout à l'heure et cela a fonctionné, donc probablement le truc du rayonnement cosmique.
Benubird

supposer que cette réponse utilise bash v4 (tableaux associatifs) et si quelqu'un essaie dans bash v3 cela ne fonctionnera pas (probablement pas ce que @Benubird a vu). Bash v3 est toujours par défaut dans de nombreux environnements
nhed

1
@nhed, point pris. Je vois que mon Macbook Yosemite à jour a la même version de base, bien que j'aie installé la v4 à partir de macports. Cette question est étiquetée "linux", mais j'ai mis à jour ma réponse pour souligner l'exigence.
ghoti

18

Je me rends compte que cela a déjà été répondu, mais cela apparaît assez haut dans les résultats de recherche et cela pourrait aider quelqu'un.

printf "%s\n" "${IDS[@]}" | sort -u

Exemple:

~> IDS=( "aa" "ab" "aa" "ac" "aa" "ad" )
~> echo  "${IDS[@]}"
aa ab aa ac aa ad
~>
~> printf "%s\n" "${IDS[@]}" | sort -u
aa
ab
ac
ad
~> UNIQ_IDS=($(printf "%s\n" "${IDS[@]}" | sort -u))
~> echo "${UNIQ_IDS[@]}"
aa ab ac ad
~>

1
pour réparer le tableau, j'ai été obligé de le faire :, ids=(ab "a a" ac aa ad ac aa);IFS=$'\n' ids2=(`printf "%s\n" "${ids[@]}" |sort -u`)alors j'ai ajouté IFS=$'\n'suggéré par @gniourf_gniourf
Aquarius Power

J'ai également dû sauvegarder et, après la commande, restaurer la valeur IFS! ou il gâche d'autres choses ..
Aquarius Power

@Jetse Cela devrait être la réponse acceptée car elle n'utilise que deux commandes, aucune boucle, aucune évaluation et est la version la plus compacte.
mgutt

1
@AquariusPower Attention, vous faites essentiellement:, IFS=$'\n'; ids2=(...)car une affectation temporaire avant les affectations de variables n'est pas possible. Au lieu d' utiliser cette construction: IFS=$'\n' read -r -a ids2 <<<"$(printf "%s\n" "${ids[@]}" | sort -u)".
Yeti

13

Si vos éléments de tableau ont un espace blanc ou tout autre caractère spécial du shell (et pouvez-vous être sûr qu'ils n'en ont pas?) Alors pour les capturer d'abord (et vous devriez toujours le faire), exprimez votre tableau entre guillemets! par exemple "${a[@]}". Bash l'interprétera littéralement comme "chaque élément du tableau dans un argument séparé ". Dans bash, cela fonctionne toujours, toujours.

Ensuite, pour obtenir un tableau trié (et unique), nous devons le convertir en un format que le tri comprend et pouvoir le reconvertir en éléments de tableau bash. C'est le meilleur que j'ai trouvé:

eval a=($(printf "%q\n" "${a[@]}" | sort -u))

Malheureusement, cela échoue dans le cas particulier du tableau vide, transformant le tableau vide en un tableau de 1 élément vide (car printf avait 0 argument mais s'imprime toujours comme s'il avait un argument vide - voir l'explication). Vous devez donc saisir cela dans un si ou quelque chose.

Explication: Le format% q pour printf "shell échappe" l'argument imprimé, de manière à ce que bash puisse récupérer dans quelque chose comme eval! Étant donné que chaque élément est imprimé avec échappement shell sur sa propre ligne, le seul séparateur entre les éléments est le saut de ligne et l'affectation du tableau prend chaque ligne comme un élément, analysant les valeurs échappées en texte littéral.

par exemple

> a=("foo bar" baz)
> printf "%q\n" "${a[@]}"
'foo bar'
baz
> printf "%q\n"
''

L'éval est nécessaire pour supprimer l'échappement de chaque valeur retournant dans le tableau.


C'est le seul code qui a fonctionné pour moi car mon tableau de chaînes contenait des espaces. Le% q est ce qui a fait le tour. Merci :)
Somaiah Kumbera

Et si vous ne souhaitez pas modifier l'ordre des éléments, utilisez à la uniqplace de sort -u.
Jesse Chisholm

Notez que uniqcela ne fonctionne pas correctement sur les listes non triées, il doit donc toujours être utilisé en combinaison avec sort.
Jean Paul

uniq sur une liste non triée supprimera les doublons consécutifs . Cela ne supprimera pas les éléments de liste identiques séparés par quelque chose d'autre entre eux. uniq peut être suffisamment utile selon les données attendues et le désir de maintenir l'ordre d'origine.
vontrapp

10

'sort' peut être utilisé pour ordonner la sortie d'une boucle for:

for i in ${ids[@]}; do echo $i; done | sort

et éliminez les doublons avec "-u":

for i in ${ids[@]}; do echo $i; done | sort -u

Enfin, vous pouvez simplement écraser votre tableau avec les éléments uniques:

ids=( `for i in ${ids[@]}; do echo $i; done | sort -u` )

Et si vous ne voulez pas changer l'ordre de ce qui reste, vous n'avez pas à:ids=( `for i in ${ids[@]}; do echo $i; done | uniq` )
Jesse Chisholm

3

celui-ci préservera également l'ordre:

echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'

et pour modifier le tableau d'origine avec les valeurs uniques:

ARRAY=($(echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'))

N'utilisez pas uniq. Il a besoin d'un tri, là où awk n'en a pas, et l'intention de cette réponse est de conserver l'ordre lorsque l'entrée n'est pas triée.
bukzor

2

Pour créer un nouveau tableau composé de valeurs uniques, assurez-vous que votre tableau n'est pas vide, puis effectuez l'une des opérations suivantes:

Supprimer les entrées en double (avec tri)

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

Supprimer les entrées en double (sans tri)

readarray -t NewArray < <(printf '%s\n' "${OriginalArray[@]}" | awk '!x[$0]++')

Attention: n'essayez pas de faire quelque chose comme NewArray=( $(printf '%s\n' "${OriginalArray[@]}" | sort -u) ). Il se brisera sur les espaces.


Supprimer les entrées en double (sans tri) est comme (avec tri) sauf changer sort -upour être uniq.
Jesse Chisholm

@JesseChisholm uniqne fusionne que les lignes en double adjacentes, donc ce n'est pas la même chose que awk '!x[$0]++'.
Six

@JesseChisholm Veuillez supprimer le commentaire trompeur.
bukzor

2

numéro de chat.txt

1 2 3 4 4 3 2 5 6

imprimer la ligne dans la colonne: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}'

1
2
3
4
4
3
2
5
6

recherchez les enregistrements en double: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk 'x[$0]++'

4
3
2

Remplacez les enregistrements en double: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk '!x[$0]++'

1
2
3
4
5
6

Rechercher uniquement les enregistrements Uniq: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i|"sort|uniq -u"}

1
5
6

1

Sans perdre la commande d'origine:

uniques=($(tr ' ' '\n' <<<"${original[@]}" | awk '!u[$0]++' | tr '\n' ' '))

1

Si vous souhaitez une solution qui n'utilise que des composants internes bash, vous pouvez définir les valeurs en tant que clés dans un tableau associatif, puis extraire les clés:

declare -A uniqs
list=(foo bar bar "bar none")
for f in "${list[@]}"; do 
  uniqs["${f}"]=""
done

for thing in "${!uniqs[@]}"; do
  echo "${thing}"
done

Cela produira

bar
foo
bar none

Je viens de remarquer que c'est essentiellement la même chose que la réponse @ghotis ci-dessus, sauf que sa solution ne prend pas en compte les éléments de liste avec des espaces.
rln

Bon point. J'ai ajouté des guillemets à ma solution pour qu'elle gère maintenant les espaces. Je l'ai initialement écrit simplement pour gérer les exemples de données dans la question, mais il est toujours bon de couvrir des contingences comme celle-ci. Merci pour la suggestion.
ghoti

1

Une autre option pour traiter les espaces blancs incorporés consiste à délimiter par null avec printf, à faire une distinction avec sort, puis à utiliser une boucle pour le remettre dans un tableau:

input=(a b c "$(printf "d\ne")" b c "$(printf "d\ne")")
output=()

while read -rd $'' element
do 
  output+=("$element")
done < <(printf "%s\0" "${input[@]}" | sort -uz)

À la fin de cela, inputet outputcontiennent les valeurs souhaitées (l'ordre à condition n'est pas important):

$ printf "%q\n" "${input[@]}"
a
b
c
$'d\ne'
b
c
$'d\ne'

$ printf "%q\n" "${output[@]}"
a
b
c
$'d\ne'

1

Et cette variation?

printf '%s\n' "${ids[@]}" | sort -u

Et puis sorted_arr=($(printf '%s\n' "${ids[@]}" | sort -u).
algues le

0

Essayez ceci pour obtenir des valeurs uniq pour la première colonne du fichier

awk -F, '{a[$1];}END{for (i in a)print i;}'

-3
# Read a file into variable
lines=$(cat /path/to/my/file)

# Go through each line the file put in the variable, and assign it a variable called $line
for line in $lines; do
  # Print the line
  echo $line
# End the loop, then sort it (add -u to have unique lines)
done | sort -u
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.