在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个回答

20

这在 bash 中运行良好:

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

10
echo $((i++)) 可以运行,并且可以将其合并到一行中。 - Bruno Bronosky
1
这里有不必要的Bash扩展。POSIX版本:https://dev59.com/HXVC5IYBdhLWcg3w1E7w#31365662 - Ciro Santilli OurBigBook.com
1
@Ciro,由于问题明确指出了bash,并且有一个bash标签,我认为你可能会发现bash的“扩展”是完全可以的 :-) - paxdiablo
@paxdiablo 我不是说这样做不正确,但既然我们可以做到可移植,为什么不呢 ;-) - Ciro Santilli OurBigBook.com
bash 中,我们可以简单地使用 while [[ i++ -le "$END" ]]; do 来在测试中进行(后置)递增。 - Aaron McDaid
显示剩余2条评论

17

有很多方法可以实现这个,但我更喜欢以下的方法

使用seq

man seq中的概述

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

语法

完整命令
seq first incr last

  • first是序列中的起始数字[可选,默认值为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 是可选的。

仅使用 first 和 last

$ 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

8
如果您想尽可能接近括号表达式语法,请尝试使用bash-tricks的range.bash中的range函数。例如,以下所有内容都将执行与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)。
其他答案在大多数情况下都有效,但它们都至少有以下一个缺点:
  • 其中许多使用子shell,这可能会影响性能并且在某些系统上可能不可行
  • 其中许多依赖于外部程序。即使是seq也是必须安装才能使用的二进制文件,必须由bash加载,并且必须包含您期望的程序才能在此情况下工作。无论普及与否,这比仅仅依赖于Bash语言本身要多得多。
  • 只使用本地Bash功能的解决方案(如@ephemient的解决方案)将无法处理字母范围,例如{a..z};但这个问题是关于数字范围的,所以这只是一个小问题。
  • 大多数解决方案在视觉上都与{1..10}花括号展开的范围语法不太相似,因此同时使用这两种程序可能会更难阅读一些。
  • @bobbogo的答案使用了一些熟悉的语法,但如果$END变量不是有效的范围“书档”,则会发生意外情况。例如,如果END=a,则不会发生错误,并将回显原始值{1..a}。这也是Bash的默认行为-通常是意外的。

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


8
这是另一种方式:
end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done

2
这会产生另一个 shell 的开销。 - codeforester
3
实际上,这是非常糟糕的,因为它会产生两个 shell,而一个就足够了。 - Bruno Bronosky

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}

7

这些方法都不错,但是seq命令已经过时了,并且大多数方法只适用于数字范围。

如果将for循环放在双引号中,当您输出字符串时,起始变量和结束变量将被解引用,然后您可以将该字符串发送回BASH以进行执行。需要使用\'进行转义以避免在发送到子shell之前对$i进行求值。

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实例,所以它适用于高强度操作。

7

{}替换为(( ))

tmpstart=0;
tmpend=4;

for (( i=$tmpstart; i<=$tmpend; i++ )) ; do 
echo $i ;
done

产生:

0
1
2
3
4

1
我已经在下面的性能比较答案中包含了这个答案。https://dev59.com/HXVC5IYBdhLWcg3w1E7w#54770805(这是一个提醒,让我知道还有哪些需要完成。) - Bruno Bronosky

6
如果您在执行Shell命令并且(像我一样)痴迷于管道,这个命令很不错: seq 1 $END | xargs -I {} echo {}

3
如果您不想使用“seq”、“eval”或“jot”或算术扩展格式(例如“for ((i=1;i<=END;i++))”)或其他循环(例如“while”),而且您只想“echo”,而不是“printf”,那么这个简单的解决方法可能适合您的预算:
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"
}

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