如何遍历Bash中变量定义的数字范围?


1541

当变量给定范围时,如何在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}


26
大家好,我在这里阅读的信息和提示都非常有帮助。我认为最好避免使用seq。原因是某些脚本需要具有可移植性,并且必须在可能不存在某些命令的各种Unix系统上运行。仅举一个例子,在FreeBSD系统上默认不存在seq。


9
我不记得是什么版本的Bash,但该命令也支持尾随零。有时真的很有帮助。命令for i in {01..10}; do echo $i; done将给出类似的数字01, 02, 03, ..., 10
topr

1
对于像我这样只想在数组的索引范围内进行迭代的人,bash方式将是:(myarray=('a' 'b' 'c'); for i in ${!myarray[@]}; do echo $i; done请注意感叹号)。它比原始问题更具体,但可以提供帮助。请参阅bash参数扩展
-PlasmaBinturong

1
括号扩展还用于{jpg,png,gif}此处未直接解决的表达式,尽管答案是相同的。看到带变量的括号扩展?[duplicate]标记为与此副本的副本。
人间

Answers:


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

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


36
seq涉及外部命令的执行,通常会降低速度。这可能无关紧要,但是如果要编写脚本来处理大量数据,则变得很重要。
paxdiablo

37
一线就好了。Pax的解决方案也很好,但是如果确实需要性能,那么我不会使用Shell脚本。
eschercycle

17
seq仅被调用一次以生成数字。exec()',除非此循环在另一个紧密循环内,否则它并不重要。
哈维尔

29
外部命令并不是真正的无关紧要的东西:如果您担心运行外部命令的开销,则根本不想使用Shell脚本,但是通常在Unix上开销很低。但是,如果END高,则存在内存使用问题。
Mark Ba​​ker

18
请注意,这seq $END是足够的,因为默认值是从1开始。从man seq:“如果省略FIRST或INCREMENT,则默认值为1”。
fedorqui'SO停止伤害

469

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));构造就像for (expr1;expr2;expr3)在C语言和类似语言中一样工作,并且与其他((expr))情况一样,Bash将其视为算术运算。


67
这样可以避免大列表的内存开销以及对的依赖seq。用它!
bobbogo 2011年

3
@MarinSagovac这确实有效,并且没有语法错误。您确定您的外壳是Bash吗?
gniourf_gniourf 2015年

3
@MarinSagovac确保确保#!/bin/bash脚本的第一行。wiki.ubuntu.com/…–
Melebius

6
只是一个很短的问题:为什么((i = 1; i <= END; i ++))和不((i = 1; i <= $ END; i ++)); 为什么在END之前没有$?
Baedsch

5
@Baedsch:由于同样的原因,我不被用作$ i。bash手册页用于算术评估的状态:“在表达式中,也可以使用名称来引用shell变量,而无需使用参数扩展语法。”
user3188140

192

讨论区

seq正如Jiaaro所建议的,使用很好。Pax Diablo建议使用Bash循环来避免调用子进程,另外的好处是,如果$ END太大,则对内存更友好。Zathrus发现了循环实现中的一个典型错误,并暗示由于这i是一个文本变量,因此在关联的减速情况下执行来回数字的连续转换。

整数算术

这是Bash循环的改进版本:

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

如果我们唯一想要的是echo,那么我们可以写echo $((i++))

短暂性教会了我一些东西:Bash允许for ((expr;expr;expr))构造。由于我从来没有阅读过Bash的整个手册页(就像我对Korn shell(ksh)手册页所做的那样,并且很久以前),所以我错过了。

所以,

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()(如call的情况seq,这是另一个映像,需要fork + exec):

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

二者evalecho都是Bash建宏,但是fork()是所必需的命令取代(所述$(…)构建体)。


1
C样式循环的唯一缺点是它不能使用命令行参数,因为它们以“ $”开头。
karatedog 2012年

3
@karatedog:for ((i=$1;i<=$2;++i)); do echo $i; done在脚本中,对于bash v.4.1.9来说,对我来说效果很好,所以我看不到命令​​行参数有问题。你还有什么意思吗
tzot 2012年

似乎eval解决方案的速度比类似C的内置解决方案快:$ time for((i = 1; i <= 100000; ++ i)); 做:; 完成i的真实0m21.220s用户0m19.763s sys 0m1.203s $时间$(eval echo“ {1..100000}”); 做:; 完成 真正的0m13.881s用户0m13.536s sys 0m0.152s
Marcin Zaluski 2012年

3
是的,但是评估是邪恶的 ……@MarcinZaluski time for i in $(seq 100000); do :; done快很多!
F. Hauri

由于eval版本在我的计算机上最快,因此性能必须特定于平台。
Andrew Prock

102

这就是为什么原始表达式不起作用的原因。

来自man bash

括号扩展在执行任何其他扩展之前执行,并且其他扩展专用的任何字符都保留在结果中。严格来说是文字。Bash对扩展的上下文或大括号之间的文本不应用任何语法解释。

因此,在参数扩展之前,大括号扩展是纯文本宏操作的早期操作

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

建议

我建议您坚持使用Posix 1功能。这意味着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-ism放入我的脚本中。脚本可能需要更快的外壳,更安全的外壳,更嵌入式的外壳。他们可能需要在安装为/ bin / sh的任何设备上运行,然后有所有常用的pro-standards参数。还记得shellshock,又名bashdoor吗?


13
我没有能力,但是我会把它移到列表的上方,首先是bash肚脐注视,但紧接着是C样式的循环和算术评估之后。
家长

2
这意味着与seq大范围扩展相比,大括号扩展不会节省太多内存。例如,echo {1..1000000} | wc显示回声产生1行,一百万个字和6,888,896字节。尝试seq 1 1000000 | wc产生一百万行,一百万个单词和6,888,896字节,并且按time命令测量,速度也快了七倍以上。
乔治

注意:我while之前在回答中提到了POSIX 方法:stackoverflow.com/a/31365662/895245但很高兴您同意:-)
Ciro Santilli冠状病毒审查六四事件法轮功

我已经在下面的性能比较答案中包含了此答案。stackoverflow.com/a/54770805/117471(这是给我自己的说明,以跟踪我剩下要做的事情。)
Bruno Bronosky

@mateor我认为循环和算术求值的C样式是相同的解决方案。我想念什么吗?
奥斯卡张

71

POSIX方式

如果您关心可移植性,请使用POSIX标准中示例

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

输出:

2
3
4
5

事情是不是 POSIX:

  • (( ))没有美元,尽管这是POSIX本身提到的常见扩展。
  • [[[在这里足够了。另请参阅:Bash中的单方括号和双方括号有什么区别?
  • for ((;;))
  • seq (GNU Coreutils)
  • {start..end},并且无法使用Bash手册中提到变量。
  • let i=i+1POSIX 7 2. Shell命令语言不包含单词let,并且在bash --posix4.3.42上失败
  • 美元i=$i+1可能是必需的,但我不确定。POSIX 7 2.6.4算术扩展说:

    如果外壳变量x包含形成有效整数常量的值,并且可以选择包括前导加号或减号,则算术扩展“ $((x))”和“ $(($ x))”应返回相同的值。值。

    但是按字面意思阅读并不意味着$((x+1))扩展,因为x+1它不是变量。


刚刚对这个答案有4次投票,这非常不寻常。如果此消息发布在某个链接聚合网站上,请给我一个链接,加油。
Ciro Santilli冠状病毒审查六四事件法轮功

引号是指x,而不是整个表达式。$((x + 1))很好
chepner '16

虽然不是可移植的,并且不同于GNU seq(BSD seq允许您使用来设置序列终止字符串-t),但是FreeBSD和NetBSD也分别seq自9.0和3.0起。
AdrianGünter'18

@CiroSantilli @chepner $((x+1))$((x + 1))解析完全相同的,因为当解析器标记化x+1将被分成3个标记:x+,和1x不是有效的数字标记,但它是有效的变量名称标记,但x+不是,因此是拆分。+是有效的算术运算符,但+1不是,因此该令牌再次在此处拆分。依此类推。
阿德里安·昆特(

我已经在下面的性能比较答案中包含了此答案。stackoverflow.com/a/54770805/117471(这是给我自己的说明,以跟踪我剩下要做的事情。)
Bruno Bronosky

35

另一层间接:

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

2
+1:此外,在{1 ..'$ END'}中为'for i评估;评估似乎是解决此问题的自然方法。
William Pursell 2011年

28

您可以使用

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

seq涉及外部命令的执行,通常会降低速度。
paxdiablo

9
它不涉及每次迭代都执行一次外部命令。如果启动一个外部命令的时间有问题,则说明您使用了错误的语言。
Mark Ba​​ker

1
那么嵌套是唯一重要的情况吗?我想知道是否存在性能差异或某些未知的技术副作用?
Sqeaky 2012年

@Squeaky这是一个单独的问题,在这里回答了以下问题:stackoverflow.com/questions/4708549/…–
Tripleee

我已经在下面的性能比较答案中包含了此答案。stackoverflow.com/a/54770805/117471(这是给我自己的说明,以跟踪我剩下要做的事情。)
Bruno Bronosky

21

如果需要前缀,则可能需要这样

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

那会产生

07
08
09
10
11
12

4
printf "%02d\n" $i比这容易printf "%2.0d\n" $i |sed "s/ /0/"吗?
zb226 '19

19

如果您使用的是BSD / OS X,则可以使用jot代替seq:

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

17

这在bash以下方面可以正常工作:

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

6
echo $((i++))工作并将其组合到一行上。
布鲁诺·布罗诺斯基


1
@Ciro,因为该问题专门指出了bash,并且具有bash标签,所以我认为您可能会发现bash的“扩展名”还不错:-)
paxdiablo 2015年

@paxdiablo我不是说这是不正确的,但为什么不能移植的时候,我们可以;-)
西罗桑蒂利冠状病毒审查六四事件法轮功

在中bash,我们可以简单地做while [[ i++ -le "$END" ]]; do测试中的(后)增量
Aaron McDaid 2015年

14

我在这里结合了一些想法并评估了性能。

TL; DR外卖:

  1. seq而且{..}真的很快
  2. forwhile循环很慢
  3. $( ) 是慢的
  4. for (( ; ; )) 循环较慢
  5. $(( )) 甚至更慢
  6. 担心内存中的N个数字(seq或{..})很愚蠢(至少一百万)。

这些不是结论。您必须查看每一个背后的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μs。这里的操作是体内的回声,算术比较,增量等。这是否令人惊讶?谁在乎循环工具需要花费多长时间来完成工作-运行时可能会受到循环内容的支配。
bobbogo

@bobbogo你是对的,这的确是关于操作数的。我更新了答案以反映这一点。实际上,我们拨打的许多电话所执行的操作超出了我们的预期。我从我运行的大约50个测试的列表中缩小了范围。我希望即使对于这个人群,我的研究也太书呆子。与往常一样,我建议像这样优先处理编码工作:缩短代码长度;使其可读;使其更快;使它易于携带。#1通常会导致#3。除非必须,否则请不要在#4上浪费时间。
布鲁诺·布鲁诺斯基

8

我知道这个问题与有关bash,但是-仅出于记录目的- ksh93更聪明,并且可以按预期实现:

$ ksh -c 'i=5; for x in {1..$i}; do echo "$x"; done'
1
2
3
4
5
$ ksh -c 'echo $KSH_VERSION'
Version JM 93u+ 2012-02-29

$ bash -c 'i=5; for x in {1..$i}; do echo "$x"; done'
{1..5}


8

如果您想尽可能地与brace-expression语法保持一致,请尝试range从bash-tricks'中获取range.bash函数

例如,以下所有操作都将与执行以下操作完全相同echo {1..10}

source range.bash
one=1
ten=10

range {$one..$ten}
range $one $ten
range {1..$ten}
range {1..10}

它尝试以尽可能少的“陷阱”来支持本机bash语法:不仅支持变量,而且for i in {1..a}; do echo $i; done还防止了无效范围通常以字符串(例如)的形式提供,这是不希望的行为。

其他答案在大多数情况下都可以使用,但是它们都至少具有以下缺点之一:

  • 它们中的许多使用子外壳,这会损害性能,并且在某些系统上可能无法实现
  • 其中许多依赖于外部程序。Even seq是必须安装才能使用的二进制文件,必须由bash加载,并且必须包含您期望的程序,这样它才能在这种情况下起作用。不论是否泛滥,它所依赖的不仅仅是Bash语言本身。
  • 仅使用本机Bash功能的解决方案(例如@ephemient的解决方案)将不适用于字母范围,例如{a..z}; 大括号扩展会。但是,问题是关于数字的范围,所以这是一个小问题。
  • 它们中的大多数在外观上都不与{1..10}大括号扩展范围语法相似,因此使用这两种语法的程序可能很难阅读。
  • @bobbogo的答案使用了一些熟悉的语法,但是如果$END变量不是该范围另一侧的有效范围“ bookend”,则会发生意外情况。END=a例如,如果使用,则不会发生错误,并且{1..a}将回显逐字记录值。这也是Bash的默认行为-经常是出乎意料的。

免责声明:我是链接代码的作者。



6

这些都很不错,但是seq被认为已被弃用,并且大多数仅适用于数字范围。

如果将for循环用双引号引起来,则在您回显字符串时,开始和结束变量将被取消引用,并且可以将字符串直接发回BASH以执行。$i需要使用\进行转义,因此在发送到子shell之前不会对其进行评估。

RANGE_START=a
RANGE_END=z
echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash

此输出也可以分配给变量:

VAR=`echo -e "for i in {$RANGE_START..$RANGE_END}; do echo \\${i}; done" | bash`

这应该产生的唯一“开销”应该是bash的第二个实例,因此它应该适合密集操作。



3

有很多方法可以做到这一点,但是下面给出了我更喜欢的方法

使用 seq

剧情简介 man seq

$ seq [-w] [-f format] [-s string] [-t string] [first [incr]] last

句法

全指令
seq first incr last

  • 第一个是序列中的起始号码[是可选的,默认情况下:1]
  • incr是增量[是可选的,默认情况下:1]
  • last是序列中的最后一个数字

例:

$ seq 1 2 10
1 3 5 7 9

只有第一个和最后一个:

$ seq 1 5
1 2 3 4 5

只有最后:

$ seq 5
1 2 3 4 5

使用 {first..last..incr}

这里的first和last是强制性的,而incr是可选的

仅使用第一个和最后一个

$ echo {1..5}
1 2 3 4 5

使用incr

$ echo {1..10..2}
1 3 5 7 9

您甚至可以将其用于以下字符

$ echo {a..z}
a b c d e f g h i j k l m n o p q r s t u v w x y z

3

如果您不想使用' seq'或' eval' jot或算术扩展格式,例如 for ((i=1;i<=END;i++)),或其他循环,例如 while,而您不想只希望“ printf”并且乐于“ echo”,那么这个简单的解决方法可能会适合您的预算:

a=1; b=5; d='for i in {'$a'..'$b'}; do echo -n "$i"; done;' echo "$d" | bash

PS:我的bash仍然没有' seq'命令。

在Mac OSX 10.6.8,Bash 3.2.48上测试


0

这适用于Bash和Korn,也可以从较高到较低的数字。可能不是最快或最漂亮,但效果很好。也处理底片。

function num_range {
   # Return a range of whole numbers from beginning value to ending value.
   # >>> num_range start end
   # start: Whole number to start with.
   # end: Whole number to end with.
   typeset s e v
   s=${1}
   e=${2}
   if (( ${e} >= ${s} )); then
      v=${s}
      while (( ${v} <= ${e} )); do
         echo ${v}
         ((v=v+1))
      done
   elif (( ${e} < ${s} )); then
      v=${s}
      while (( ${v} >= ${e} )); do
         echo ${v}
         ((v=v-1))
      done
   fi
}

function test_num_range {
   num_range 1 3 | egrep "1|2|3" | assert_lc 3
   num_range 1 3 | head -1 | assert_eq 1
   num_range -1 1 | head -1 | assert_eq "-1"
   num_range 3 1 | egrep "1|2|3" | assert_lc 3
   num_range 3 1 | head -1 | assert_eq 3
   num_range 1 -1 | tail -1 | assert_eq "-1"
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.