在Bash中循环遍历元组

129
在Bash中是否可以循环遍历元组?
举个例子,如果以下代码能够正常工作就太好了:
for (i,j) in ((c,3), (e,5)); do echo "$i and $j"; done

有没有一种方法可以让我以某种方式循环遍历元组?

7
作为来自 Python 背景的人,这确实是一个非常有用的问题! - John Jiang
5
看着这个四年后,我想知道是否还没有更好的方法来做这件事。天啊。 - Giszmo
将近8年过去了,我也在想是否还有更好的方法来做这件事。但是,这个2018年的答案对我来说看起来非常不错:https://dev59.com/i2kw5IYBdhLWcg3wrsrn#52228219 - MountainX
12个回答

107
$ 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

8
很好 - 只想指出,如果在命令行上运行此代码,则应保存并重置IFS的原始值。另外,在循环运行之前只需设置一次新的IFS,而不是每次迭代都设置。 - 0eggxactly
1
如果任何一个 $i 以连字符开头,最好使用 set -- $i - glenn jackman
2
不要保存IFS,只需为set命令设置它:for i in c,3 e,5; do IFS="," set -- $i; echo $1 and $2; done。请编辑您的答案:如果所有读者都只选择列出的解决方案之一,那么阅读完整的开发历史就没有意义了。感谢这个酷炫的技巧! - cfi
2
如果我声明tuples="a,1 b,2 c,3"并在编辑版本中设置IFS=',',并且使用$tuples而不是c,3 e,5,则它根本无法正常打印。 但是,如果我将IFS=','放在for循环中的do关键字之后,那么当使用$tuples以及文字值时,它就能够正常工作。只是觉得值得一提。 - Simonlbc
@Simonlbc 这是因为 for 循环使用 IFS 来分割迭代。例如,如果您循环遍历一个数组 arr=("c,3" "e,5") 并在 for 循环之前放置 IFS,那么 $i 的值将只是 ce,它会分离掉 35,所以 set 不会正确解析,因为 $i 没有任何要解析的内容。这意味着,如果要迭代的值不是内联的,则应该将 IFS 放在循环内部,并且外部值应该遵守要迭代的变量的预期分隔符。在 $tuples 的情况下,它应该是简单的 IFS=,这是默认值,并在空格上拆分。 - untore

76
根据Eduardo Ivanec提供的答案,不需要设置/重置IFS,可以简单地执行以下操作:
for i in "c 3" "e 5"
do
    set -- $i # Convert the "tuple" into the param args $1 $2...
    echo $1 and $2
done

输出内容:
c and 3
e and 5

3
这种方法对我来说比被接受和获得最多赞的方法简单得多。与Eduardo Ivanec建议的方式相比,有没有任何原因不这样做呢? - spurra
@spurra 这个答案是6年半以后的,基于它。功劳归于功臣。 - Diego
1
@Diego 我知道这一点。答案中明确写着。我想知道是否有任何理由不使用这种方法而选择接受的答案。 - spurra
4
如果默认的分隔符(空格、制表符或换行符)对你来说由于某种原因无法使用,那么你会希望使用 Eduardo 给出的答案。(https://bash.cyberciti.biz/guide/$IFS) - Diego
如果您在函数中使用了 $1$2 作为参数传递,那么您该怎么办呢?也就是说,如何区分传递的第一个和第二个参数与元组的第一个和第二个项目? - mfaani
显示剩余2条评论

54
这个Bash风格指南演示了如何使用read来在分隔符处拆分字符串并将它们赋值给单独的变量。因此,使用这种技术,你可以像下面的循环中的一行代码那样解析字符串并赋值给变量。
for i in c,3 e,5; do
    IFS=',' read item1 item2 <<< "${i}"
    echo "${item1}" and "${item2}"
done

20
使用一个关联数组(也称为字典和哈希映射):
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

1
顺便说一下,这个方法在我的 Mac 上没有生效,我使用的是通过 brew 安装的 GNU bash 版本为 4.4.23(1)-release-(x86_64-apple-darwin17.5.0),可能因人而异。 - David
它在 Ubuntu 14.04 中的 Docker 容器内运行,但在 GNU bash, version 4.3.11(1)-release-(x86_64-pc-linux-gnu) 上工作。 - David
似乎旧版本的Bash或不支持此功能的版本是基于索引的,其中键是数字而不是字符串。请参阅http://tldp.org/LDP/abs/html/declareref.html,而我们使用的选项是“-a”,而不是“-A”。 - David
大卫,是这样的。我认为你可以尝试使用数组索引来获取“结合性”。例如 declare -a indices=(1 2 3); declare -a sound=(barks purrs cheeps); declare -a size=(big medium small) 等等。虽然我还没有在终端中尝试过,但我认为它应该可以工作。 - VasiliNovikov
1
看起来关联数组的键的迭代顺序是随机的,所以要小心... https://dev59.com/UF4b5IYBdhLWcg3wbBCB - rogerdpack
1
@rogerdpack 谢谢!我已经删除了与非确定性迭代顺序有关的部分,当前的解决方案应该始终按照您定义的方式进行迭代。 - VasiliNovikov

9
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)) 是什么?

$((something)) 是算术扩展,所以 $((3+4)) 是 7 等等。我们的表达式是 ${#c[*]}-1,因此 something - 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。


2
你应该执行 for i in $(seq 0 $(($#c[*]}-1))); do [...] - reox
2
哇,这真是今天我见过的“最丑陋的一堆随意字符”奖项的获得者。有人能解释一下这个可怕的东西到底是做什么的吗?我在井号那里就迷失了... - koniiiik
1
@koniiiik:已添加解释。 - user unknown
"Neu-Perl: The Revenge" 我认为用 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

6

但是如果元组比关联数组可以容纳的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

现在元素的数量已经没有限制了。不是在寻求投票,只是随便想想而已。参考1参考2


这个适用于sh。由于较新版本的macOS停止更新bash,因此此版本更适合针对macOS的脚本。 - WeZZard
有一些类似bourne的shell值得探索: 1) bash(比原版更丰富功能),2) dash(原版的posix兼容版本)。对于每个新的macOS安装,我建议使用brew install shellcheck bash dash bash-completion@2。这将获得最新的GNU bash - 就像在Linux上一样; 所以无论哪里都是相同的 :-) - todd_dsm
Bash自动补全仅在您仍然使用bash作为系统shell echo $SHELL时才是必需的。在新的macOS上,它应该是ZSH;在这种情况下,请给自己一个方便并安装Oh My ZSH - 它更加出色。 - todd_dsm

4
使用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 - WestCoastProjects
2
brew install parallel - WestCoastProjects
或许应该透露所属关系?例如,来自*如何避免成为垃圾邮件发送者的内容:“您必须在帖子中披露您的所属关系”。还请参阅回答中自我推广的限制如何处理开源存储库的推广关于回答中稀疏自我推广的政策是什么?*,- - undefined
这个图书馆的推广是垃圾信息吗?和自我推广滥用被发现了吗? - undefined

2
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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接