Eseguire il loop delle tuple in bash?


89

È possibile eseguire il ciclo su tuple in bash?

Ad esempio, sarebbe fantastico se funzionasse quanto segue:

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

Esiste una soluzione alternativa che in qualche modo mi consenta di eseguire il ciclo su tuple?


4
Venendo dallo sfondo di Python, questa è davvero una domanda molto utile!
John Jiang

5
guardando questo quattro anni dopo mi chiedo se non ci sia ancora modo migliore per farlo. Oh mio Dio.
Giszmo

Quasi 8 anni dopo mi sono anche chiesto se non ci fosse ancora modo migliore per farlo. Ma questa risposta del 2018 mi sembra piuttosto buona: stackoverflow.com/a/52228219/463994
MountainX

Risposte:


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

A proposito di questo uso di set(da man builtins):

Tutti gli argomenti rimanenti dopo l'elaborazione delle opzioni vengono trattati come valori per i parametri posizionali e vengono assegnati, nell'ordine, a $ 1, $ 2, ... $ n

Gli IFS=","insiemi il separatore di campo così ogni $iviene segmentato in $1e$2 correttamente.

Tramite questo blog .

Modifica: versione più corretta, come suggerito da @SLACEDIAMOND:

$ 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
Bello: voglio solo sottolineare che IFSdovrebbe essere salvato e ripristinato al suo valore originale se questo viene eseguito sulla riga di comando. Inoltre, il nuovo IFSpuò essere impostato una volta, prima che il ciclo venga eseguito, anziché ogni iterazione.
Eggx esattamente il

1
Nel caso in cui uno qualsiasi dei $ i inizi con un trattino, è più sicuroset -- $i
glenn jackman,

1
Invece di risparmio IFS, impostare solo per il setcomando: for i in c,3 e,5; do IFS="," set -- $i; echo $1 and $2; done. Modifica la tua risposta: se tutti i lettori scegliessero solo una delle soluzioni elencate, non ha senso dover leggere l'intera cronologia dello sviluppo. Grazie per questo fantastico trucco!
cfi

Se dichiaro tuples="a,1 b,2 c,3"e metto IFS=','come nella versione modificata, e invece di c,3 e,5usarlo $tuplesnon stampa affatto bene. Ma invece se metto IFS=','subito dopo la doparola chiave nel ciclo for, funziona bene quando si utilizzano $tuplesvalori così come letterali. Ho solo pensato che valesse la pena dirlo.
Simonlbc

@ Simonlbc è perché il ciclo for utilizza IFSper dividere le iterazioni. Ad esempio, se esegui un ciclo su un array come arr=("c,3" "e,5")e lo metti IFSprima del ciclo for, il valore di $isarà solo ce e, verrà suddiviso 3e 5quindi setnon verrà analizzato correttamente perché $inon avrà nulla da analizzare. Ciò significa che se i valori da iterare non sono inline, il IFSdeve essere inserito nel ciclo e il valore esterno deve rispettare il separatore previsto per la variabile su cui iterare. In questi casi $tuplesdovrebbe essere semplicemente il IFS=valore predefinito e suddiviso in spazi bianchi.
untore

26

Credo che questa soluzione sia un po 'più pulita delle altre che sono state presentate, h / t a questa guida di stile bash per illustrare come read può essere usato per dividere le stringhe in un delimitatore e assegnarle a singole variabili.

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

20

Sulla base della risposta data da @ eduardo-ivanec senza impostare / resettare IFS, si potrebbe semplicemente fare:

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

Il risultato:

c and 3
e and 5

Questo approccio mi sembra molto più semplice dell'approccio accettato e più votato. C'è qualche motivo per non farlo in questo modo rispetto a quanto suggerito da @Eduardo Ivanec?
spurra

@spurra questa risposta è di 6 anni e mezzo più recente e si basa su di essa. Credito dove è dovuto.
Diego

1
@Diego ne sono consapevole. È scritto esplicitamente nella risposta. Chiedevo se ci fosse qualche ragione per non utilizzare questo approccio rispetto alla risposta accettata.
spurra

2
@spurra vorresti usare la risposta di Eduardo se il separatore predefinito (spazio, tabulazione o nuova riga) non funziona per te per qualche motivo ( bash.cyberciti.biz/guide/$IFS )
Diego

13

Usa array associativo (noto anche come dizionario / hashMap):

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

Funziona per bash4.0 +.


Se hai bisogno di triple invece delle coppie, puoi utilizzare l'approccio più generale:

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

Cordiali saluti, questo non ha funzionato per me su Mac con GNU bash, version 4.4.23(1)-release-(x86_64-apple-darwin17.5.0), che è stato installato tramite brew, quindi YMMV.
David

tuttavia ha funzionato GNU bash, version 4.3.11(1)-release-(x86_64-pc-linux-gnu)da Ubuntu 14.04 all'interno del contenitore docker.
David

sembra che le versioni precedenti di bash o quelle che non supportano questa funzionalità funzionino con l'indice? dove la chiave è un numero anziché una stringa. tldp.org/LDP/abs/html/declareref.html , e invece -Aabbiamo -a.
David

David, sembra così. Penso che puoi provare gli indici degli array per ottenere "associatività" allora. Come declare -a indices=(1 2 3); declare -a sound=(barks purrs cheeps); declare -a size=(big medium small)ecc. Non l'ho ancora provato nel terminale, ma penso che dovrebbe funzionare.
VasiliNovikov

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

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

A volte potrebbe essere più utile.

Per spiegare la uglyparte, come notato nei commenti:

seq 0 2 produce la sequenza di numeri 0 1 2. $ (cmd) è la sostituzione del comando, quindi per questo esempio l'output di seq 0 2, che è la sequenza numerica. Ma qual è il limite superiore, il $((${#c[*]}-1))?

$ ((qualcosa)) è l'espansione aritmetica, quindi $ ((3 + 4)) è 7 ecc. La nostra espressione è ${#c[*]}-1, quindi qualcosa - 1. Abbastanza semplice, se sappiamo cos'è ${#c[*]}.

c è un array, c [*] è solo l'intero array, $ {# c [*]} è la dimensione dell'array che è 2 nel nostro caso. Ora ripristiniamo tutto: for i in $(seq 0 $((${#c[*]}-1)))è for i in $(seq 0 $((2-1)))è for i in $(seq 0 1)è for i in 0 1. Perché l'ultimo elemento dell'array ha un indice che è la lunghezza dell'array - 1.


1
dovresti farlofor i in $(seq 0 $(($#c[*]}-1))); do [...]
reox

1
Wow, questo vince il premio "Il più brutto mazzo di personaggi arbitrari che abbia mai visto oggi". Qualcuno vuole spiegare cosa fa esattamente questo abominio? Mi sono perso al cancelletto ...
koniiiik

1
@koniiiik: spiegazione aggiunta.
utente sconosciuto

"Neu-Perl: The Revenge" Penso che in realtà sarebbe più chiaro in perl. :-)
Buzz Moschetti

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

Utilizzo di GNU Parallel:

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

O:

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

O:

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

1
Nessun amore per questo? ben mi ha fatto superare l'inerzia e installaregnu parallel
StephenBoesch

2
brew install parallel
StephenBoesch

2

Utilizzo printfin una sostituzione di processo:

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

Sopra richiede bash. Se bashnon viene utilizzato, utilizzare la pipeline semplice:

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

1
Sì! Anubhava, dolce genio!
Scott

1
do echo $key $value
done < file_discriptor

per esempio:

$ 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

Un po 'più complicato, ma può essere utile:

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

Ma cosa succede se la tupla è maggiore del k / v che può contenere un array associativo? E se fossero 3 o 4 elementi? Si potrebbe espandere questo concetto:

###---------------------------------------------------
### 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

Quindi l'output sarebbe simile a:

$ ./assoc-array-tinkering.sh 

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

E il numero di elementi ora è illimitato. Non sto cercando voti; solo pensando ad alta voce. RIF1 , RIF2


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.