Wie kann ich eindeutige Werte aus einem Array in Bash abrufen?


86

Ich habe fast die gleiche Frage wie hier .

Ich habe ein Array, das aa ab aa ac aa adusw. enthält . Jetzt möchte ich alle eindeutigen Elemente aus diesem Array auswählen. Dachte, dies wäre einfach mit sort | uniqoder mit, sort -uwie sie in dieser anderen Frage erwähnt haben, aber nichts hat sich im Array geändert ... Der Code lautet:

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

Was mache ich falsch?

Antworten:


127

Ein bisschen hacky, aber das sollte es tun:

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

Führen Sie die Array-Zuweisung aus, um die sortierten eindeutigen Ergebnisse wieder in einem Array zu speichern :

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

Wenn Ihre Shell Herestrings unterstützt ( bashsollte), können Sie einen echoProzess sparen , indem Sie ihn ändern in:

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

Eingang:

ids=(aa ab aa ac aa ad)

Ausgabe:

aa ab ac ad

Erläuterung:

  • "${ids[@]}"- Syntax für die Arbeit mit Shell-Arrays, unabhängig davon, ob sie als Teil echooder als Herestring verwendet werden. Der @Teil bedeutet "alle Elemente im Array"
  • tr ' ' '\n'- Konvertieren Sie alle Leerzeichen in Zeilenumbrüche. Weil Ihr Array von der Shell als Elemente in einer einzelnen Zeile gesehen wird, die durch Leerzeichen getrennt sind. und weil sort erwartet, dass die Eingabe in separaten Zeilen erfolgt.
  • sort -u - nur eindeutige Elemente sortieren und beibehalten
  • tr '\n' ' ' - Konvertieren Sie die zuvor hinzugefügten Zeilenumbrüche wieder in Leerzeichen.
  • $(...)- Befehlsersetzung
  • Nebenbei: tr ' ' '\n' <<< "${ids[@]}"ist eine effizientere Methode:echo "${ids[@]}" | tr ' ' '\n'

33
+1. Ein bisschen aufgeräumter: Speichern Sie Uniq-Elemente in einem neuen Array:uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}"
Glenn Jackman

@glennjackman oh ordentlich! Ich wusste nicht einmal, dass Sie auf printfdiese Weise verwenden können (geben Sie mehr Argumente als
Formatzeichenfolgen an

4
+1 Ich bin mir nicht sicher, ob dies ein Einzelfall ist, aber für das Zurücksetzen eindeutiger Elemente in ein Array sind zusätzliche Klammern erforderlich, z sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')). Ohne die zusätzlichen Klammern wurde es als Zeichenfolge angegeben.
Whla

3
Wenn Sie die Reihenfolge der Elemente nicht ändern möchten, verwenden Sie ... | uniq | ...anstelle von ... | sort -u | ....
Jesse Chisholm

2
@Jesse, uniqentfernt nur aufeinanderfolgende Duplikate. Im Beispiel in dieser Antwort sorted_unique_idswird am Ende identisch mit dem Original ids. Versuchen Sie es, um Ordnung zu erhalten ... | awk '!seen[$0]++'. Siehe auch stackoverflow.com/questions/1444406/… .
Rob Kennedy

26

Wenn Sie Bash Version 4 oder höher ausführen (was in jeder modernen Linux-Version der Fall sein sollte), können Sie eindeutige Array-Werte in bash erhalten, indem Sie ein neues assoziatives Array erstellen, das jeden der Werte des ursprünglichen Arrays enthält. Etwas wie das:

$ 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

Dies funktioniert, weil in jedem Array (assoziativ oder traditionell, in jeder Sprache) jeder Schlüssel nur einmal angezeigt werden kann. Wenn die forSchleife den zweiten Wert von aain erreicht a[2], überschreibt sie b[aa]den ursprünglich für a[0].

Das Ausführen von Dingen in nativem Bash kann schneller sein als das Verwenden von Pipes und externen Tools wie z. B. sortund uniqbei größeren Datensätzen werden Sie wahrscheinlich eine bessere Leistung erzielen, wenn Sie eine leistungsfähigere Sprache wie awk, python usw. verwenden.

Wenn Sie sich sicher fühlen, können Sie die forSchleife vermeiden , indem Sie die printfMöglichkeit nutzen, das Format für mehrere Argumente zu recyceln, obwohl dies anscheinend erforderlich ist eval. (Hör jetzt auf zu lesen, wenn du damit einverstanden bist.)

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

Der Grund, den diese Lösung erfordert, evalbesteht darin, dass Array-Werte vor der Wortteilung ermittelt werden. Dies bedeutet, dass die Ausgabe der Befehlssubstitution als einzelnes Wort und nicht als Satz von Schlüssel-Wert-Paaren betrachtet wird.

Während dies eine Subshell verwendet, werden nur Bash-Buildins verwendet, um die Array-Werte zu verarbeiten. Stellen Sie sicher, dass Sie Ihre Verwendung evalmit kritischem Auge bewerten . Wenn Sie nicht zu 100% sicher sind, dass chepner, glenn jackman oder greycat keinen Fehler an Ihrem Code finden, verwenden Sie stattdessen die for-Schleife.


erzeugt Fehler: Ausdrucksrekursionsstufe überschritten
Benubird

1
@ Benubird - kannst du vielleicht deinen Terminalinhalt einfügen? Es funktioniert perfekt für mich, also ist meine beste Vermutung, dass Sie (1) einen Tippfehler, (2) eine ältere Version von Bash (assoziative Arrays wurden zu v4 hinzugefügt) oder (3) einen lächerlich großen Zustrom von kosmischem Hintergrund haben Strahlung, die durch das Quantenschwarze Loch im Keller Ihres Nachbarn verursacht wird und Interferenzen mit den Signalen in Ihrem Computer erzeugt.
Ghoti

1
kann nicht, hat nicht den behalten, der nicht funktioniert hat. Aber ich habe gerade versucht, deine zu betreiben, und es hat funktioniert, also wahrscheinlich die Sache mit der kosmischen Strahlung.
Benubird

Vermutung, dass diese Antwort bash v4 (assoziative Arrays) verwendet und wenn jemand in bash v3 versucht, wird es nicht funktionieren (wahrscheinlich nicht das, was @Benubird gesehen hat). Bash v3 noch in vielen ENVs Standard ist
nhed

1
@nhed, Punkt genommen. Ich sehe, dass mein aktuelles Yosemite Macbook dieselbe Version in der Basis hat, obwohl ich v4 von Macports installiert habe. Diese Frage ist mit "Linux" gekennzeichnet, aber ich habe meine Antwort aktualisiert, um auf die Anforderung hinzuweisen.
Ghoti

16

Mir ist klar, dass dies bereits beantwortet wurde, aber es wurde ziemlich häufig in den Suchergebnissen angezeigt, und es könnte jemandem helfen.

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

Beispiel:

~> 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
Um das Array zu reparieren ids=(ab "a a" ac aa ad ac aa);IFS=$'\n' ids2=(`printf "%s\n" "${ids[@]}" |sort -u`), musste ich IFS=$'\n'Folgendes tun : , also fügte ich hinzu, vorgeschlagen von @gniourf_gniourf
Aquarius Power

Ich musste auch den IFS-Wert sichern und nach dem Befehl wiederherstellen! oder es bringt andere Dinge durcheinander ..
Aquarius Power

@Jetse Dies sollte die akzeptierte Antwort sein, da nur zwei Befehle verwendet werden, keine Schleifen, keine Auswertung und die kompakteste Version.
mgutt

1
@AquariusPower Vorsicht, Sie tun im Grunde: IFS=$'\n'; ids2=(...)da eine temporäre Zuweisung vor variablen Zuweisungen nicht möglich ist. Verwenden Sie stattdessen diese Konstruktion : IFS=$'\n' read -r -a ids2 <<<"$(printf "%s\n" "${ids[@]}" | sort -u)".
Yeti

12

Wenn Ihre Array-Elemente Leerzeichen oder andere Shell-Sonderzeichen haben (und können Sie sicher sein, dass dies nicht der Fall ist?), Um Ihr Array zu erfassen (und Sie sollten dies einfach immer tun), drücken Sie Ihr Array in doppelten Anführungszeichen aus! zB "${a[@]}". Bash interpretiert dies wörtlich als "jedes Array-Element in einem separaten Argument ". Innerhalb von Bash funktioniert das einfach immer, immer.

Um ein sortiertes (und eindeutiges) Array zu erhalten, müssen wir es in ein Format konvertieren, das die Sortierung versteht, und es wieder in Bash-Array-Elemente konvertieren können. Dies ist das Beste, was ich mir ausgedacht habe:

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

Leider schlägt dies im Sonderfall des leeren Arrays fehl und verwandelt das leere Array in ein Array mit 1 leeren Element (da printf 0 Argumente hatte, aber immer noch so druckt, als hätte es ein leeres Argument - siehe Erklärung). Also muss man das in einem Wenn oder so fangen.

Erläuterung: Das% q-Format für printf "shell entgeht" dem gedruckten Argument, so dass bash in so etwas wie eval wiederhergestellt werden kann! Da jedes Element in einer eigenen Zeile als Shell gedruckt wird, ist das einzige Trennzeichen zwischen den Elementen die neue Zeile. Bei der Array-Zuweisung wird jede Zeile als Element verwendet, wobei die maskierten Werte in Literaltext analysiert werden.

z.B

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

Die Auswertung ist erforderlich, um das Escapezeichen von jedem Wert zu entfernen, der in das Array zurückkehrt.


Dies ist der einzige Code, der für mich funktioniert hat, da mein String-Array Leerzeichen enthält. Das% q hat den Trick gemacht. Danke :)
Somaiah Kumbera

Und wenn Sie die Reihenfolge der Elemente nicht ändern möchten, verwenden Sie uniqstattdessen sort -u.
Jesse Chisholm

Beachten Sie, dass uniqdies bei unsortierten Listen nicht ordnungsgemäß funktioniert. Daher muss es immer in Kombination mit verwendet werden sort.
Jean Paul

uniq in einer unsortierten Liste entfernt aufeinanderfolgende Duplikate. Es werden keine identischen Listenelemente entfernt, die durch etwas anderes dazwischen getrennt sind. uniq kann abhängig von den erwarteten Daten und dem Wunsch, die ursprüngliche Reihenfolge beizubehalten, nützlich genug sein.
vontrapp

8

'sort' kann verwendet werden, um die Ausgabe einer for-Schleife zu ordnen:

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

und eliminiere Duplikate mit "-u":

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

Schließlich können Sie Ihr Array einfach mit den eindeutigen Elementen überschreiben:

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

Und wenn Sie die Reihenfolge der ids=( `for i in ${ids[@]}; do echo $i; done | uniq` )
Jesse Chisholm

3

Dieser wird auch die Ordnung bewahren:

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

und um das ursprüngliche Array mit den eindeutigen Werten zu ändern:

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

Nicht benutzen uniq. Es muss sortiert werden, wo awk dies nicht tut, und die Absicht dieser Antwort ist es, die Reihenfolge beizubehalten, wenn die Eingabe unsortiert ist.
Bukzor

2

Stellen Sie zum Erstellen eines neuen Arrays mit eindeutigen Werten sicher, dass Ihr Array nicht leer ist, und führen Sie einen der folgenden Schritte aus:

Doppelte Einträge entfernen (mit Sortierung)

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

Doppelte Einträge entfernen (ohne zu sortieren)

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

Warnung: Versuchen Sie nicht, so etwas zu tun NewArray=( $(printf '%s\n' "${OriginalArray[@]}" | sort -u) ). Es wird auf Leerzeichen brechen.


Das Entfernen doppelter Einträge (ohne Sortieren) ist genau wie (mit Sortieren), außer dass Änderungen vorgenommen sort -uwerden müssen uniq.
Jesse Chisholm

@JesseChisholm führt uniqnur benachbarte doppelte Zeilen zusammen, es ist also nicht dasselbe wie awk '!x[$0]++'.
Sechs

@JesseChisholm Bitte um irreführenden Kommentar zu löschen.
Bukzor

2

cat number.txt

1 2 3 4 4 3 2 5 6

Zeile in Spalte drucken: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}'

1
2
3
4
4
3
2
5
6

Finden Sie die doppelten Datensätze: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk 'x[$0]++'

4
3
2

Doppelte Datensätze ersetzen: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk '!x[$0]++'

1
2
3
4
5
6

Nur Uniq-Datensätze finden: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i|"sort|uniq -u"}

1
5
6

1

Ohne die ursprüngliche Bestellung zu verlieren:

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

1

Wenn Sie eine Lösung wünschen, die nur Bash-Interna verwendet, können Sie die Werte als Schlüssel in einem assoziativen Array festlegen und dann die Schlüssel extrahieren:

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

Dies wird ausgegeben

bar
foo
bar none

Mir ist gerade aufgefallen, dass dies im Wesentlichen mit der obigen Antwort von @ghotis identisch ist, außer dass seine Lösung Listenelemente mit Leerzeichen nicht berücksichtigt.
Am

Guter Punkt. Ich habe meiner Lösung Anführungszeichen hinzugefügt, damit sie jetzt Leerzeichen verarbeitet. Ich habe es ursprünglich nur geschrieben, um die Beispieldaten in der Frage zu behandeln, aber es ist immer gut, solche Eventualitäten abzudecken. Danke für den Vorschlag.
Ghoti

1

Eine weitere Option für den Umgang mit eingebetteten Leerzeichen besteht darin, sie durch Nullen zu begrenzen printf, zu unterscheiden sortund dann mithilfe einer Schleife wieder in ein Array zu packen:

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)

Am Ende inputund outputenthalten Sie die gewünschten Werte (vorausgesetzt, die Reihenfolge ist nicht wichtig):

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

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

1

Wie wäre es mit dieser Variante?

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

Und dann sorted_arr=($(printf '%s\n' "${ids[@]}" | sort -u).
Algen vor

0

Versuchen Sie dies, um eindeutige Werte für die erste Spalte in der Datei zu erhalten

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.