Петля над кортежами в bash?


88

Чи можна циклічно перебирати кортежі в bash?

Як приклад, було б чудово, якби працювало наступне:

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

Чи є спосіб вирішення проблеми, який якось дозволяє мені перебирати кортежі?


4
Виходячи з фону python, це справді дуже корисне питання!
Джон Цзян,

5
дивлячись на це через чотири роки, я задаюся питанням, чи не існує кращого способу зробити це. О Боже мій.
Giszmo

Майже через 8 років я також подумав, чи не існує кращого способу зробити це. Але ця відповідь 2018 року мені здається досить непоганою: stackoverflow.com/a/52228219/463994
MountainX

Відповіді:


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

Про це використання set(від man builtins):

Будь-які аргументи, що залишаються після обробки параметрів, обробляються як значення для позиційних параметрів і присвоюються, відповідно, $ 1, $ 2, ... $ n

IFS=","Встановлює роздільник полів так що кожен $iотримує сегментований в $1і $2правильно.

Через цей блог .

Редагувати: більш правильна версія, як запропонував @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
Приємно - просто хочу вказати, що його IFSслід зберегти та повернути до початкового значення, якщо це запустити в командному рядку. Крім того, нове IFSможна встановити один раз, перед запуском циклу, а не кожну ітерацію.
Яскраво

1
Якщо будь-який із $ i починається з дефіса, це безпечнішеset -- $i
glenn jackman

1
Замість економії IFS, тільки встановити його для setкоманди: for i in c,3 e,5; do IFS="," set -- $i; echo $1 and $2; done. Будь ласка, відредагуйте свою відповідь: Якщо всі читачі виберуть лише одне з перерахованих рішень, немає сенсу читати повну історію розробок. Дякую за цей класний трюк!
cfi

Якщо я оголошую tuples="a,1 b,2 c,3"і ставлю, IFS=','як у відредагованій версії, і замість c,3 e,5використання $tuplesце взагалі погано друкується. Але замість цього, якщо я вклав IFS=','одразу після doключового слова у цикл for, він добре працює, коли використовується $tuples, а також літеральні значення. Просто думав, що варто сказати.
Simonlbc

@Simonlbc це тому, що цикл for використовує IFSдля розділення ітерацій. тобто якщо ви прокручуєте масив типу like arr=("c,3" "e,5")і ставите IFSперед циклом for, значення $iбуде справедливим cі e, воно розділиться 3і 5тому setне буде аналізуватись правильно, тому що $iне буде чого розбирати. Це означає, що якщо значення для ітерації не вбудовані, значення IFSслід помістити всередину циклу, а зовнішнє значення повинно відповідати призначеному роздільнику для змінної, для якої потрібно виконати ітерацію. У випадках $tuplesце повинно бути просто, IFS=що є типовим і розбивається на пробіли.
зняти з пам’ятки

25

Я вважаю, що це рішення трохи чистіше, ніж інші, що були представлені, h / t до цього посібника для стилю bash, щоб проілюструвати, як читання можна використовувати для розділення рядків на роздільник та присвоєння їх окремим змінним.

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

17

На основі відповіді, даної @ eduardo-ivanec, не встановлюючи / не скидаючи IFS, можна просто зробити:

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

Вихід:

c and 3
e and 5

Мені такий підхід здається набагато простішим, ніж прийнятий та найбільш прихильний підхід. Чи є підстави не робити цього таким чином, на відміну від того, що пропонував @Eduardo Ivanec?
spurra

@spurra ця відповідь на 6 років і ½ пізніша і базується на ній. Кредит там, де це потрібно.
Дієго

1
@Diego Я це знаю. Це чітко написано у відповіді. Я запитував, чи є якісь причини не використовувати цей підхід над прийнятою відповіддю.
spurra

2
@spurra, ви хотіли б використати відповідь Едуардо, якщо роздільник за замовчуванням (пробіл, вкладка чи новий рядок) з якихось причин не працює для вас ( bash.cyberciti.biz/guide/$IFS )
Дієго,

11

Використовуйте асоціативний масив (також відомий як словник / 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

Працює для bash4.0 +.


Якщо вам потрібні трійки замість пар, ви можете використовувати більш загальний підхід:

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

FYI, це не спрацювало у мене на Mac з GNU bash, version 4.4.23(1)-release-(x86_64-apple-darwin17.5.0), який був встановлений за допомогою brew, тому YMMV.
Девід

однак він працював GNU bash, version 4.3.11(1)-release-(x86_64-pc-linux-gnu)з Ubuntu 14.04 в контейнері докера.
Девід

здається, що старіші версії bash або версії, що не підтримують цю функцію, працюють на базі індексу? де ключ - це число, а не рядок. tldp.org/LDP/abs/html/declareref.html , а замість цього -Aми маємо -a.
Девід

Девіде, здається. Я думаю, ви можете спробувати індекси масивів, щоб отримати "асоціативність" тоді. Як і declare -a indices=(1 2 3); declare -a sound=(barks purrs cheeps); declare -a size=(big medium small)т. Д. Ще не пробував у терміналі, але я думаю, що це має спрацювати.
ВасильНовіков

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

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

Іноді може бути більш зручним.

Для пояснення uglyчастини, як зазначено в коментарях:

seq 0 2 створює послідовність чисел 0 1 2. $ (cmd) є підстановкою команди, тому для цього прикладу виводиться seq 0 2, що є послідовністю чисел. Але що таке верхня межа $((${#c[*]}-1)),?

$ ((Somthing)) - це арифметичне розширення, тому $ ((3 + 4)) дорівнює 7 і т. д. Наш вираз є ${#c[*]}-1, отже, щось - 1. Досить просто, якщо ми знаємо, що ${#c[*]}таке.

c - це масив, c [*] - це просто весь масив, $ {# c [*]} - це розмір масиву, який у нашому випадку дорівнює 2. Тепер відкочуємо все назад: 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. Оскільки останній елемент масиву має індекс, який є довжиною масиву - 1.


1
ви повинні це зробитиfor i in $(seq 0 $(($#c[*]}-1))); do [...]
reox

1
Ого, це виграє нагороду «Найкрасивіша купа довільних персонажів, яких я бачив сьогодні». Хтось хоче пояснити, що саме робить ця гидота? Я загубився на знаку хеш ...
koniiiik

1
@koniiiik: Пояснення додано.
невідомий користувач

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

Використання GNU Parallel:

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

Або:

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

Або:

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

1
Немає любові до цього? добре змусив мене подолати інерцію та встановитиgnu parallel
StephenBoesch

2
brew install parallel
StephenBoesch

2

Використання printfв процесі заміни:

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

Вище вимагає bash. Якщо bashвін не використовується, використовуйте простий конвеєр:

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

1
Так! Анубхава, ти солодкий генію!
Скотт

1
do echo $key $value
done < file_discriptor

наприклад:

$ 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

Трохи більше залученого, але може бути корисним:

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

Але що, якщо кортеж більше k / v, який може вмістити асоціативний масив? Що робити, якщо це 3 або 4 елементи? Можна розширити цю концепцію:

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

Тоді результат виглядав би приблизно так:

$ ./assoc-array-tinkering.sh 

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

І кількість елементів тепер без обмежень. Не шукає голосів; просто думаючи вголос. REF1 , REF2


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.