在Bash中,我如何迭代一个由变量定义的数字范围?

2143
如何在Bash中迭代一个由变量给定的数字范围?
我知道可以这样做(在Bash文档中称为“序列表达式”):
for i in {1..5}; do echo $i; done

这是什么意思:
1 2 3 4 5
然而,我如何用变量替换范围的任一端点?这样做不起作用:
END=5
for i in {1..$END}; do echo $i; done

这将打印:

{1..5}


43
大家好,我在这里读到的信息和提示都非常有帮助。我认为最好避免使用seq命令。原因是一些脚本需要具备可移植性并能在各种不同的Unix系统上运行,而某些命令可能不存在。举个例子,在FreeBSD系统中默认情况下并没有seq命令。 - user557212
12
我不记得从哪个版本的Bash开始支持尾随零,但这个命令也支持。有时候这真的很有帮助。命令for i in {01..10}; do echo $i; done会输出像01, 02, 03, ..., 10这样的数字。 - topr
4
对于像我这样只想迭代数组索引范围的人,Bash 的方法是:myarray=('a' 'b' 'c'); for i in ${!myarray[@]}; do echo $i; done(注意感叹号)。 这比原来的问题更具体,但可能会有所帮助。 参见 bash 参数扩展 - PlasmaBinturong
1
@PhoenixMu,你可以使用bash yourscript.sh而不是sh yourscript.sh来运行shell脚本。 - Xin Niu
显示剩余5条评论
20个回答

2374
for i in $(seq 1 $END); do echo $i; done

编辑:我更喜欢使用seq方法,因为我可以真正记住它 ;)


61
seq包括执行外部命令,这通常会减慢运行速度。如果你正在编写用于处理大量数据的脚本,这可能并不重要,但它变得很重要。 - paxdiablo
59
一句话来说就很好。Pax的解决方案也可以,但如果性能真的是问题的话,我不会使用shell脚本。 - eschercycle
22
seq 只被调用一次以生成数字。使用exec()在该循环不是在另一个紧密循环内时不应有显著影响。 - Javier
34
外部命令并不是很重要:如果你担心运行外部命令的开销,你就不应该使用shell脚本,但通常在Unix系统上,这种开销很低。然而,如果END值很高,则存在内存使用问题。 - Mark Baker
28
请注意,seq $END已足够,因为默认情况下从1开始。根据man seq命令的解释:“如果省略FIRSTINCREMENT参数,则默认为1”。 - fedorqui
显示剩余8条评论

727

seq方法是最简单的,但是Bash内置了算术求值。

END=5
for ((i=1;i<=END;i++)); do
    echo $i
done
# ==> outputs 1 2 3 4 5 on separate lines

for ((expr1;expr2;expr3)); 结构与 C 和类似语言中的 for (expr1;expr2;expr3) 完全相同,并且像其他((expr))情况一样,Bash 将它们视为算术表达式。


114
使用这种方法可以避免大型列表的内存开销,并且不依赖于seq。使用它! - bobbogo
5
请确保在脚本的第一行添加 #!/bin/bash。具体操作请参考该页面:https://wiki.ubuntu.com/DashAsBinSh#My_production_system_has_broken_and_I_just_want_to_get_it_back_up.21 - Melebius
14
只是一个非常简短的问题:为什么是 ((i=1;i<=END;i++)) 而不是 ((i=1;i<=$END;i++))?为什么 END 前面没有 $ 符号? - Baedsch
16
由于相同的原因,$i并不会被用作$i。Bash man页面说明了在算术表达式中: "在表达式中,也可以通过名称引用shell变量,而无需使用参数扩展语法。" - user3188140
6
我已将此答案包含在下面的性能比较答案中。https://dev59.com/HXVC5IYBdhLWcg3w1E7w#54770805(这是一条便笺,用于记录我还有哪些工作要做。)我特意创建了那个答案来回应@bobbogo的不合格主张和那些点赞它的人。剧透:大型列表的内存开销并不像Bash中c-style循环的性能缓慢那样糟糕。如果您有想法,请在那里发表评论。让我们不要劫持这个线程。 - Bruno Bronosky
显示剩余5条评论

219

讨论

如Jiaaro建议的那样,使用seq是可以的。Pax Diablo建议使用Bash循环来避免调用子进程,还有一个额外的好处是如果$END太大,则更节省内存。Zathrus发现了循环实现中的一个典型错误,并暗示由于i是一个文本变量,因此需要进行连续的数字转换,这会导致慢速运行。

整数算术

这是Bash循环的改进版本:

typeset -i i END
let END=5 i=1
while ((i<=END)); do
    echo $ilet i++
done

如果我们只想要得到echo,那么我们可以编写echo $((i++))

ephemient告诉了我一个知识点:Bash允许使用for ((expr;expr;expr))结构。因为我从来没有读完整个Bash的man页面(像我以前读过的Korn shell (ksh) man页面一样,而那是很久以前的事情了),所以我错过了这个知识点。

所以,

typeset -i i END # Let's be explicit
for ((i=1;i<=END;++i)); do echo $i; done

这似乎是最节省内存的方法(不需要分配内存来消耗seq的输出,如果END非常大可能会成为一个问题),尽管可能不是“最快”的。

初始问题

eschercycle指出,{a..b} Bash符号仅适用于字面值;根据Bash手册,这是正确的。可以通过单个(内部)fork()而没有exec()来克服这个障碍(就像调用seq一样,它作为另一个映像需要一个fork+exec):

for i in $(eval echo "{1..$END}"); do

evalecho都是Bash内置命令,但对于命令替换($(...)结构),需要使用fork()


1
C风格循环的唯一缺点是无法使用命令行参数,因为它们以"$"开头。 - karatedog
4
在Bash脚本中,使用for ((i=$1;i<=$2;++i)); do echo $i; done命令可以正常工作。因此,我认为命令行参数没有问题。你是指其他方面的问题吗? - tzot
1
似乎eval解决方案比内置的类C语言for循环更快:$ time for ((i=1;i<=100000;++i)); do :; done实际 0分21.220秒 用户 0分19.763秒 系统 0分1.203秒 $ time for i in $(eval echo "{1..100000}"); do :; done;实际 0分13.881秒 用户 0分13.536秒 系统 0分0.152秒 - Marcin Zaluski
3
是的,但是 eval 是邪恶的... @MarcinZaluski time for i in $(seq 100000); do :; done 更快! - F. Hauri - Give Up GitHub
性能必须是特定于平台的,因为在我的机器上,评估版本最快。 - Andrew Prock
测试了所有的方法,只有它们被支持,即使是MacOS的默认/bin/sh(破折号,而不是Bash)。 - Top-Master

120

这里是原始表达式不起作用的原因。

根据man bash

在执行任何其他扩展之前,都会进行花括号扩展,并且对于其他扩展特殊的字符会保留在结果中。它是纯文本的宏操作。Bash 不会对扩展的上下文或括号之间的文本应用任何语法解释。

因此,花括号扩展是一种早期的纯文本宏操作,在参数扩展之前执行。

Shell是高度优化的宏处理器和更正式的编程语言之间的混合体。为了优化典型使用情况,语言变得更加复杂并接受一些限制。

建议

我建议坚持使用Posix1功能。如果列表已知,请使用 for i in <list>; do,否则请使用 whileseq,如下所示:

#!/bin/sh

limit=4

i=1; while [ $i -le $limit ]; do
  echo $i
  i=$(($i + 1))
done
# Or -----------------------
for i in $(seq 1 $limit); do
  echo $i
done


1. Bash是一个很好的shell,我也会在交互式中使用它,但我不会在我的脚本中添加bash特性。脚本可能需要更快的shell、更安全的shell或嵌入式风格的shell。它们可能需要在已安装为/bin/sh的任何系统上运行,还有所有通常的专业标准论点。记得shellshock,又称bashdoor吗?


14
我没有权力,但我希望将这个东西在列表中往前移一些,超过所有关于自我陶醉的闲扯,但紧接着 C 风格的 for 循环和算术运算。 - mateor
2
一个含义是,与seq相比,花括号扩展在大范围内并不能节省太多的内存。例如,echo {1..1000000} | wc 显示 echo 产生了 1 行、一百万个单词和 6,888,896 字节。尝试 seq 1 1000000 | wc 得到了一百万行、一百万个单词和 6,888,896 字节,并且使用 time 命令测量,速度也快了七倍以上。 - George
注意:我之前在我的回答中提到了POSIX的while方法:https://dev59.com/HXVC5IYBdhLWcg3w1E7w#31365662 但很高兴你同意 :-) - Ciro Santilli OurBigBook.com
我已经在下面的性能比较答案中包含了这个答案。https://dev59.com/HXVC5IYBdhLWcg3w1E7w#54770805(这是一个提醒,让我知道还有哪些需要完成。) - Bruno Bronosky
@mateor 我认为C风格的for循环和算术运算是同样的解决方案。我有什么遗漏吗? - ychz
我喜欢C风格的for循环或while循环。从bash的范围扩展中放弃的一个特性是使用{01..99}进行零填充。但是,printf(1)可以做到同样的效果。在for循环中,尝试使用printf“%02i \ n”$ i而不是echo。对于while循环方法,请尝试printf“%02i \ n”$ ((i ++))。格式字符串:“0”表示用零填充而不是空格,“2”是字段宽度,“i”表示整数,不要忘记换行符:) - JGurtz

102

POSIX的方式

如果你关心可移植性,使用来自POSIX标准的示例

i=2
end=5
while [ $i -le $end ]; do
    echo $i
    i=$(($i+1))
done

输出:

2
3
4
5

不符合 POSIX 标准的内容:


刚刚在这个答案上获得了4个赞,这非常不寻常。如果这是发布在某个链接聚合网站上的,请给我一个链接,谢谢。 - Ciro Santilli OurBigBook.com
引用是指 x,而不是整个表达式。$((x + 1)) 是完全正确的。 - chepner
虽然不具备可移植性,并且与GNU的seq有所不同(BSD的seq允许您使用-t设置序列终止字符串),但自FreeBSD 9.0和NetBSD 3.0以来,它们也拥有了seq - Adrian Günter
@CiroSantilli @chepner $((x+1))$((x + 1)) 解析的结果完全相同,因为当解析器将 x+1 分词时,它将被分成3个标记:x+1x 不是一个有效的数字标记,但它是一个有效的变量名标记,然而 x+ 不是,因此被分开了。+ 是一个有效的算术运算符标记,但 +1 不是,所以标记再次被分开。依此类推。 - Adrian Günter
我已经在下面的性能比较答案中包含了这个答案。https://dev59.com/HXVC5IYBdhLWcg3w1E7w#54770805(这是一个提醒,让我知道还有哪些需要完成。) - Bruno Bronosky

42

您可以使用

for i in $(seq $END); do echo $i; done

seq 包含执行外部命令,通常会减慢程序速度。 - paxdiablo
13
每次迭代不涉及执行外部命令,只需执行一次。如果启动一个外部命令的时间是个问题,那么你在使用错误的语言。 - Mark Baker
1
那么嵌套是唯一需要考虑的情况吗?我在想是否存在性能差异或某些未知的技术副作用? - Sqeaky
@Squeaky 这是一个单独的问题,答案在这里:https://dev59.com/iG445IYBdhLWcg3wws4u - tripleee
我已经在下面的性能比较答案中包含了这个答案。https://dev59.com/HXVC5IYBdhLWcg3w1E7w#54770805(这是一个提醒,让我知道还有哪些需要完成。) - Bruno Bronosky

39

另一层间接性:

for i in $(eval echo {1..$END}); do
    ∶

3
+1:此外,eval 'for i in {1..'$END'}; do ... ' 看起来是解决这个问题的自然方式。 - William Pursell
不错的技巧!如果你想要一个前导零,这个方法很有效。for n in {001..10}; do echo $n; done - undefined

33

我将这里的一些想法结合在一起并进行了性能测试。

TL;DR 要点:

  1. seq{..} 非常快
  2. forwhile 循环很慢
  3. $( ) 很慢
  4. for (( ; ; )) 循环更慢
  5. $(( )) 更慢
  6. 担心内存中的N个数字(seq或{..})是愚蠢的(至少在100万以下)。

这些不是结论。您必须查看每个机制背后的C代码才能得出结论。这更多地涉及我们如何使用每个机制来循环代码。大多数单个操作在速度上足够接近,以至于在大多数情况下不会有影响。但是像for (( i=1; i<=1000000; i++ ))这样的机制是许多操作,正如您可以看到的那样。它还比您从for i in $(seq 1 1000000)获得的每个循环的操作多得多。这可能对您不明显,这就是为什么进行此类测试很有价值。

演示

# show that seq is fast
$ time (seq 1 1000000 | wc)
 1000000 1000000 6888894

real    0m0.227s
user    0m0.239s
sys     0m0.008s

# show that {..} is fast
$ time (echo {1..1000000} | wc)
       1 1000000 6888896

real    0m1.778s
user    0m1.735s
sys     0m0.072s

# Show that for loops (even with a : noop) are slow
$ time (for i in {1..1000000} ; do :; done | wc)
       0       0       0

real    0m3.642s
user    0m3.582s
sys 0m0.057s

# show that echo is slow
$ time (for i in {1..1000000} ; do echo $i; done | wc)
 1000000 1000000 6888896

real    0m7.480s
user    0m6.803s
sys     0m2.580s

$ time (for i in $(seq 1 1000000) ; do echo $i; done | wc)
 1000000 1000000 6888894

real    0m7.029s
user    0m6.335s
sys     0m2.666s

# show that C-style for loops are slower
$ time (for (( i=1; i<=1000000; i++ )) ; do echo $i; done | wc)
 1000000 1000000 6888896

real    0m12.391s
user    0m11.069s
sys     0m3.437s

# show that arithmetic expansion is even slower
$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $i; i=$(($i+1)); done | wc)
 1000000 1000000 6888896

real    0m19.696s
user    0m18.017s
sys     0m3.806s

$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $i; ((i=i+1)); done | wc)
 1000000 1000000 6888896

real    0m18.629s
user    0m16.843s
sys     0m3.936s

$ time (i=1; e=1000000; while [ $i -le $e ]; do echo $((i++)); done | wc)
 1000000 1000000 6888896

real    0m17.012s
user    0m15.319s
sys     0m3.906s

# even a noop is slow
$ time (i=1; e=1000000; while [ $((i++)) -le $e ]; do :; done | wc)
       0       0       0

real    0m12.679s
user    0m11.658s
sys 0m1.004s

1
不错!但我不同意你的总结。在我看来,$(seq) 的速度与 {a..b} 差不多。此外,每个操作所需的时间也大致相同,因此对于每次循环迭代,它会增加约 4 微秒的时间。这里的操作是指循环体中的 _echo_、算术比较、递增等。这其中有什么令人惊讶的吗?谁在乎循环机制需要多长时间才能完成其工作——运行时很可能会被循环内容所主导。 - bobbogo
@bobbogo 你说得对,这确实与操作次数有关。我更新了我的答案以反映这一点。我们进行的许多调用实际上执行的操作比我们预期的要多。我从我运行的大约50个测试列表中缩小了范围。我本来以为我的研究甚至对这个群体来说都太书呆子了。像往常一样,我建议优先考虑编码工作:使其更短;使其可读;使其更快;使其可移植。通常情况下,#1会导致#3。在必要之前不要浪费时间在#4上。 - Bruno Bronosky
这是一个有趣的练习,尽管最初的问题是关于使用变量控制迭代,例如{...}不允许。 - logicOnAbstractions
我从来不知道 {i..n} 这个用法!太酷了,每种编程语言都应该有这个功能。 - renardesque

25

如果您需要前缀,那么您可能会喜欢这个。

 for ((i=7;i<=12;i++)); do echo `printf "%2.0d\n" $i |sed "s/ /0/"`;done

将产生

07
08
09
10
11
12

6
printf "%02d\n" $iprintf "%2.0d\n" $i |sed "s/ /0/" 更简单易懂吗? - zb226

21

如果你在BSD / OS X上,可以使用jot替代seq:

for i in $(jot $END); do echo $i; done

macOS具有seq命令。 - dcow
The seq command first appeared in Plan 9 from Bell Labs. A seq command appeared in NetBSD 3.0, and ported to FreeBSD 9.0. This command was based on the command of the same name in Plan 9 from Bell Labs and the GNU core utilities. The GNU seq command first appeared in the 1.13 shell utilities release. - dcow

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