Schleife über Tupel in Bash?


88

Ist es möglich, Tupel in Bash zu durchlaufen?

Als Beispiel wäre es großartig, wenn Folgendes funktionieren würde:

for (i,j) in ((c,3), (e,5)); do echo "$i and $j"; done

Gibt es eine Problemumgehung, mit der ich Tupel durchlaufen kann?


4
Aus dem Python-Hintergrund kommend ist dies in der Tat eine sehr nützliche Frage!
John Jiang

5
Wenn ich mir das vier Jahre später ansehe, frage ich mich, ob es noch keinen besseren Weg gibt, dies zu tun. Oh mein Gott.
Giszmo

Fast 8 Jahre später fragte ich mich auch, ob es noch keinen besseren Weg gibt, dies zu tun. Aber diese Antwort für 2018 sieht für mich ziemlich gut aus: stackoverflow.com/a/52228219/463994
MountainX

Antworten:


86
$ for i in c,3 e,5; do IFS=","; set -- $i; echo $1 and $2; done
c and 3
e and 5

Über diese Verwendung von set(von man builtins):

Alle nach der Optionsverarbeitung verbleibenden Argumente werden als Werte für die Positionsparameter behandelt und in der Reihenfolge $ 1, $ 2, ... $ n zugewiesen

Das IFS=","setzt das Feldtrennzeichen so, dass jedes $iin $1und $2korrekt segmentiert wird.

Über diesen Blog .

Bearbeiten: korrektere Version, wie von @SLACEDIAMOND vorgeschlagen:

$ OLDIFS=$IFS; IFS=','; for i in c,3 e,5; do set -- $i; echo $1 and $2; done; IFS=$OLDIFS
c and 3
e and 5

7
Schön - möchte nur darauf hinweisen, IFSsollte gespeichert und auf den ursprünglichen Wert zurückgesetzt werden, wenn dies in der Befehlszeile ausgeführt wird. Außerdem kann das Neue IFSeinmal festgelegt werden, bevor die Schleife ausgeführt wird, und nicht bei jeder Iteration.
Genau

1
Falls einer der $ i mit einem Bindestrich beginnt, ist es sicherer,set -- $i
Glenn Jackman

1
Anstatt zu speichern IFS, setzen Sie es nur für den setBefehl : for i in c,3 e,5; do IFS="," set -- $i; echo $1 and $2; done. Bitte bearbeiten Sie Ihre Antwort: Wenn alle Leser nur eine der aufgelisteten Lösungen auswählen würden, macht es keinen Sinn, den vollständigen Entwicklungsverlauf lesen zu müssen. Danke für diesen coolen Trick!
cfi

Wenn ich deklariere tuples="a,1 b,2 c,3"und IFS=','wie in der bearbeiteten Version setze und anstatt zu c,3 e,5verwenden $tuples, druckt es überhaupt nicht gut. Wenn ich jedoch IFS=','direkt nach dem doSchlüsselwort in die for-Schleife setze , funktioniert dies sowohl bei der Verwendung $tuplesals auch bei der Verwendung von Wurfwerten. Ich dachte nur, es lohnt sich zu sagen.
Simonlbc

@Simonlbc, weil die for-Schleife IFSzum Teilen von Iterationen verwendet wird. dh wenn Sie eine Schleife über ein Array wie arr=("c,3" "e,5")und IFSvor die for-Schleife stellen, ist der Wert von $inur cund e, es wird aufgeteilt 3und wird 5daher setnicht korrekt analysiert, da $inichts zu analysieren ist. Dies bedeutet, dass, wenn die zu iterierenden Werte nicht inline sind, die Werte in IFSdie Schleife eingefügt werden sollen und der externe Wert das beabsichtigte Trennzeichen für die zu iterierende Variable berücksichtigen sollte. In den Fällen $tuplessollte es einfach sein, IFS=was Standard ist und auf Leerzeichen aufteilt.
Ungerissen

25

Ich glaube, diese Lösung ist ein wenig sauberer als die anderen, die eingereicht wurden. H / t in diesem Bash-Styleguide, um zu veranschaulichen, wie read verwendet werden kann, um Zeichenfolgen an einem Trennzeichen zu teilen und sie einzelnen Variablen zuzuweisen.

for i in c,3 e,5; do 
    IFS=',' read item1 item2 <<< "${i}"
    echo "${item1}" and "${item2}"
done

17

Basierend auf der Antwort von @ eduardo-ivanec ohne Einstellen / Zurücksetzen von IFSkönnte man einfach Folgendes tun:

for i in "c 3" "e 5"
do
    set -- $i
    echo $1 and $2
done

Die Ausgabe:

c and 3
e and 5

Dieser Ansatz scheint mir viel einfacher zu sein als der akzeptierte und am besten bewertete Ansatz. Gibt es einen Grund, es nicht so zu machen, im Gegensatz zu dem, was @Eduardo Ivanec vorgeschlagen hat?
spurra

@spurra Diese Antwort ist 6 Jahre und ½ jünger und basiert darauf. Gutschrift, wo es fällig ist.
Diego

1
@Diego das ist mir bewusst. Es ist ausdrücklich in der Antwort geschrieben. Ich habe gefragt, ob es einen Grund gibt, diesen Ansatz nicht gegenüber der akzeptierten Antwort zu verwenden.
spurra

2
@spurra Sie möchten Eduardos Antwort verwenden, wenn das Standardtrennzeichen (Leerzeichen, Tabulator oder Zeilenumbruch) aus irgendeinem Grund für Sie nicht funktioniert ( bash.cyberciti.biz/guide/$IFS )
Diego,

10

Verwenden Sie ein assoziatives Array (auch als Dictionary / HashMap bezeichnet):

declare -A pairs=(
  [c]=3
  [e]=5
)
for key in "${!pairs[@]}"; do
  value="${pairs[$key]}"
  echo "key is $key and value is $value"
done

Funktioniert für bash4.0 +.


Wenn Sie Tripel anstelle von Paaren benötigen, können Sie den allgemeineren Ansatz verwenden:

animals=(dog cat mouse)
declare -A sound=(
  [dog]=barks
  [cat]=purrs
  [mouse]=cheeps
)
declare -A size=(
  [dog]=big
  [cat]=medium
  [mouse]=small
)
for animal in "${animals[@]}"; do
  echo "$animal ${sound[$animal]} and it is ${size[$animal]}"
done

Zu GNU bash, version 4.4.23(1)-release-(x86_64-apple-darwin17.5.0)Ihrer Information , dies funktionierte nicht für mich auf einem Mac mit , der über Brew installiert wurde, also YMMV.
David

es hat jedoch funktioniert GNU bash, version 4.3.11(1)-release-(x86_64-pc-linux-gnu) ab Ubuntu 14.04 im Docker-Container.
David

scheint, als würden ältere Versionen von Bash oder solche, die diese Funktion nicht unterstützen, die Index-Basierung abarbeiten? Dabei ist der Schlüssel eher eine Zahl als eine Zeichenfolge. tldp.org/LDP/abs/html/declareref.html , und stattdessen -Ahaben wir-a .
David

David, scheint so. Ich denke, Sie können dann Array-Indizes ausprobieren, um "Assoziativität" zu erhalten. Mögendeclare -a indices=(1 2 3); declare -a sound=(barks purrs cheeps); declare -a size=(big medium small) usw. Ich habe es noch nicht im Terminal ausprobiert, aber ich denke, es sollte funktionieren.
VasiliNovikov

7
c=('a' 'c')
n=(3    4 )

for i in $(seq 0 $((${#c[*]}-1)))
do
    echo ${c[i]} ${n[i]}
done

Könnte manchmal praktischer sein.

Um den uglyTeil zu erklären , wie in den Kommentaren angegeben:

seq 0 2 erzeugt die Folge von Zahlen 0 1 2. $ (cmd) ist eine Befehlssubstitution, also für dieses Beispiel die Ausgabe von seq 0 2, die die Zahlenfolge ist. Aber was ist die Obergrenze, die $((${#c[*]}-1))?

$ ((etwas)) ist eine arithmetische Erweiterung, also ist $ ((3 + 4)) 7 usw. Unser Ausdruck ist ${#c[*]}-1also etwas - 1. Ziemlich einfach, wenn wir wissen, was ${#c[*]}ist.

c ist ein Array, c [*] ist nur das gesamte Array, $ {# c [*]} ist die Größe des Arrays, in unserem Fall 2. Jetzt rollen wir alles zurück:for i in $(seq 0 $((${#c[*]}-1))) ist for i in $(seq 0 $((2-1)))ist for i in $(seq 0 1)ist for i in 0 1. Weil das letzte Element im Array einen Index hat, der der Länge des Arrays entspricht - 1.


1
Sie sollten tunfor i in $(seq 0 $(($#c[*]}-1))); do [...]
reox

1
Wow, dies gewinnt die Auszeichnung „Hässlichster Haufen willkürlicher Charaktere, die ich heute gesehen habe“. Möchte jemand erklären, was genau dieser Gräuel bewirkt? Ich habe mich am Hash-Zeichen verlaufen ...
koniiiik

1
@koniiiik: Erklärung hinzugefügt.
Benutzer unbekannt

6
$ echo 'c,3;e,5;' | while IFS=',' read -d';' i j; do echo "$i and $j"; done
c and 3
e and 5

3

Verwenden von GNU Parallel:

parallel echo {1} and {2} ::: c e :::+ 3 5

Oder:

parallel -N2 echo {1} and {2} ::: c 3 e 5

Oder:

parallel --colsep , echo {1} and {2} ::: c,3 e,5

1
Keine Liebe dazu? gut gemacht mich Trägheit zu überwinden und zu installierengnu parallel
Javadba

2
brew install parallel
Javadba

2

Verwendung printfin einer Prozesssubstitution:

while read -r k v; do
    echo "Key $k has value: $v"
done < <(printf '%s\n' 'key1 val1' 'key2 val2' 'key3 val3')

Key key1 has value: val1
Key key2 has value: val2
Key key3 has value: val3

Oben erfordert bash. Wenn bashes nicht verwendet wird, verwenden Sie eine einfache Pipeline:

printf '%s\n' 'key1 val1' 'key2 val2' 'key3 val3' |
while read -r k v; do echo "Key $k has value: $v"; done

1
Ja! Anubhava, du süßes Genie!
Scott

1
do echo $key $value
done < file_discriptor

beispielsweise:

$ while read key value; do echo $key $value ;done <<EOF
> c 3
> e 5
> EOF
c 3
e 5

$ echo -e 'c 3\ne 5' > file

$ while read key value; do echo $key $value ;done <file
c 3
e 5

$ echo -e 'c,3\ne,5' > file

$ while IFS=, read key value; do echo $key $value ;done <file
c 3
e 5

0

Ein bisschen komplizierter, kann aber nützlich sein:

a='((c,3), (e,5))'
IFS='()'; for t in $a; do [ -n "$t" ] && { IFS=','; set -- $t; [ -n "$1" ] && echo i=$1 j=$2; }; done

0

Was aber, wenn das Tupel größer ist als das k / v, das ein assoziatives Array enthalten kann? Was ist, wenn es 3 oder 4 Elemente sind? Man könnte dieses Konzept erweitern:

###---------------------------------------------------
### VARIABLES
###---------------------------------------------------
myVars=(
    'ya1,ya2,ya3,ya4'
    'ye1,ye2,ye3,ye4'
    'yo1,yo2,yo3,yo4'
    )


###---------------------------------------------------
### MAIN PROGRAM
###---------------------------------------------------
### Echo all elements in the array
###---
printf '\n\n%s\n' "Print all elements in the array..."
for dataRow in "${myVars[@]}"; do
    while IFS=',' read -r var1 var2 var3 var4; do
        printf '%s\n' "$var1 - $var2 - $var3 - $var4"
    done <<< "$dataRow"
done

Dann würde die Ausgabe ungefähr so ​​aussehen:

$ ./assoc-array-tinkering.sh 

Print all elements in the array...
ya1 - ya2 - ya3 - ya4
ye1 - ye2 - ye3 - ye4
yo1 - yo2 - yo3 - yo4

Und die Anzahl der Elemente ist jetzt unbegrenzt. Keine Suche nach Stimmen; nur laut denken. REF1 , REF2


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.