当范围由变量给出时,如何迭代 Bash 中的数字范围?
我知道我可以做到这一点(在 Bash documentation 中称为“序列表达式”):
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}
for i in {01..10}; do echo $i; done
会给出类似 01, 02, 03, ..., 10
的数字。
myarray=('a' 'b' 'c'); for i in ${!myarray[@]}; do echo $i; done
(注意感叹号)。它比原始问题更具体,但可能会有所帮助。请参阅bash parameter expansions
{jpg,png,gif}
这样的表达式,这里没有直接解决,尽管答案是相同的。请参阅Brace expansion with variable? [duplicate],它被标记为与此重复。
for i in $(seq 1 $END); do echo $i; done
编辑:我更喜欢 seq
而不是其他方法,因为我实际上可以记住它;)
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 将它们视为算术。
seq
的依赖。用它!
#!/bin/bash
设为脚本的第一行。 wiki.ubuntu.com/…
讨论
正如 Jiaaro 建议的那样,使用 seq
很好。 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++))
。
ephemient 教会了我一些东西: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 手册。可以使用没有 exec()
的单个(内部)fork()
来克服这个障碍(就像调用 seq
的情况一样,这是另一个需要 fork+exec 的图像):
for i in $(eval echo "{1..$END}"); do
eval
和 echo
都是 Bash 内置函数,但命令替换需要 fork()
($(…)
构造)。
for ((i=$1;i<=$2;++i)); do echo $i; done
在 bash v.4.1.9 上对我来说效果很好,所以我看不到命令行参数有问题。你的意思是别的吗?
time for i in $(seq 100000); do :; done
快得多!
这就是为什么原始表达式不起作用的原因。
来自 man bash:
大括号扩展在任何其他扩展之前执行,并且任何其他扩展的特殊字符都保留在结果中。它是严格的文本。 Bash 不对扩展的上下文或大括号之间的文本应用任何句法解释。
因此,大括号扩展是在参数扩展之前作为纯文本宏操作尽早完成的。
Shell 是宏处理器和更正式的编程语言之间高度优化的混合体。为了优化典型用例,语言变得相当复杂,并且接受了一些限制。
推荐
我建议坚持使用 Posix1 功能。这意味着如果列表已知,则使用 for i in <list>; do
,否则使用 while
或 seq
,如下所示:
#!/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-isms 放入我的脚本中。脚本可能需要更快的 shell、更安全的 shell、更嵌入式的 shell。他们可能需要在安装为 /bin/sh 的任何东西上运行,然后有所有常见的支持标准的参数。还记得 shellshock,又名 bashdoor 吗?
seq
相比,大括号扩展并没有节省太多内存。例如,echo {1..1000000} | wc
表明回显产生 1 行、100 万字和 6,888,896 字节。尝试 seq 1 1000000 | wc
会产生一百万行、一百万字和 6,888,896 字节,并且速度也快了七倍以上,如 time
命令所测量的那样。
while
方法:stackoverflow.com/a/31365662/895245 但很高兴您同意 :-)
POSIX 方式
如果您关心可移植性,请使用 example from the POSIX standard:
i=2
end=5
while [ $i -le $end ]; do
echo $i
i=$(($i+1))
done
输出:
2
3
4
5
不是 POSIX 的东西:
(( )) 没有美元,尽管它是 POSIX 本身提到的常见扩展。
[[。 [这里就够了。另请参阅:Bash 中的单方括号和双方括号有什么区别?
为了 ((;;))
seq (GNU Coreutils)
{start..end},并且不能与 Bash 手册中提到的变量一起使用。
let i=i+1: POSIX 7 2. Shell 命令语言不包含单词 let,并且在 bash --posix 4.3.42 上失败
可能需要 i=$i+1 的美元,但我不确定。 POSIX 7 2.6.4 算术扩展说:如果 shell 变量 x 包含一个形成有效整数常量的值,可选地包括前导加号或减号,那么算术扩展 "$((x))" 和 "$(( $x))" 应返回相同的值。但是从字面上阅读它并不意味着 $((x+1)) 扩展,因为 x+1 不是变量。
x
,而不是整个表达式。 $((x + 1))
很好。
seq
不同(BSD seq
允许您使用 -t
设置序列终止字符串),但 FreeBSD 和 NetBSD 从 9.0 和 3.0 开始也分别具有 seq
。
$((x+1))
和 $((x + 1))
解析完全相同,因为当解析器标记 x+1
时,它将分为 3 个标记:x
、+
和 1
。 x
不是有效的数字标记,但它是有效的变量名标记,但 x+
不是,因此拆分。 +
是一个有效的算术运算符记号,但 +1
不是,所以记号在此处再次被拆分。等等。
您可以使用
for i in $(seq $END); do echo $i; done
另一层间接:
for i in $(eval echo {1..$END}); do
∶
如果你需要它的前缀,那么你可能会喜欢这个
for ((i=7;i<=12;i++)); do echo `printf "%2.0d\n" $i |sed "s/ /0/"`;done
这将产生
07
08
09
10
11
12
printf "%02d\n" $i
不会比 printf "%2.0d\n" $i |sed "s/ /0/"
更容易吗?
我在这里结合了一些想法并测量了性能。
TL;DR 要点:
seq 和 {..} 非常快 for 而 while 循环很慢 $( ) 很慢 for (( ; ; )) 循环更慢 $(( )) 甚至更慢 担心内存中有 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
$(seq)
的速度与 {a..b}
大致相同。此外,每个操作都需要大约相同的时间,因此对我来说,循环的每次迭代增加了大约 4μs。这里的操作是主体中的echo、算术比较、增量等。这有什么令人惊讶的吗?谁在乎循环用具完成它的工作需要多长时间——运行时很可能由循环的内容主导。
如果你在 BSD / OS X 上,你可以使用 jot 而不是 seq:
for i in $(jot $END); do echo $i; done
seq
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.
这在 bash
中运行良好:
END=5
i=1 ; while [[ $i -le $END ]] ; do
echo $i
((i = i + 1))
done
echo $((i++))
起作用并将其合并到一行中。
bash
中,我们可以简单地执行 while [[ i++ -le "$END" ]]; do
来执行测试中的(后)增量
有很多方法可以做到这一点,但我更喜欢的方法如下
使用序列
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 是可选的
只使用第一个和最后一个
$ echo {1..5}
1 2 3 4 5
使用增量
$ 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
我知道这个问题是关于 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}
这是另一种方式:
end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done
如果您想尽可能接近大括号表达式语法,请尝试 range
function from 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
)的常见不良行为。
其他答案在大多数情况下都有效,但它们都至少具有以下缺点之一:
他们中的许多人使用子shell,这可能会损害性能,并且在某些系统上可能是不可能的。
他们中的许多人依赖外部程序。甚至 seq 也是必须安装才能使用的二进制文件,必须由 bash 加载,并且必须包含您期望的程序,才能在这种情况下工作。不管是否无处不在,这不仅仅依赖于 Bash 语言本身。
仅使用本机 Bash 功能的解决方案(例如 @ephemient 的)将不适用于字母范围,例如 {a..z};大括号展开会。不过,问题是关于数字的范围,所以这是一个小问题。
它们中的大多数在视觉上与 {1..10} 大括号扩展范围语法并不相似,因此使用两者的程序可能会有点难以阅读。
@bobbogo 的答案使用了一些熟悉的语法,但如果 $END 变量不是范围另一侧的有效范围“书挡”,则会出现意想不到的情况。例如,如果 END=a,则不会发生错误,并且会回显逐字值 {1..a}。这也是 Bash 的默认行为——只是经常出乎意料。
免责声明:我是链接代码的作者。
这些都很好,但 seq 据说已被弃用,并且大多数仅适用于数字范围。
如果你用双引号将你的 for 循环括起来,当你回显字符串时,开始和结束变量将被取消引用,你可以将字符串直接发送回 BASH 执行。 $i
需要用 \'s 转义,因此在发送到子 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 的第二个实例,因此它应该适合密集型操作。
将 {}
替换为 (( ))
:
tmpstart=0;
tmpend=4;
for (( i=$tmpstart; i<=$tmpend; i++ )) ; do
echo $i ;
done
产量:
0
1
2
3
4
如果您正在执行 shell 命令,并且您(像我一样)喜欢流水线,那么这个很好:
seq 1 $END | xargs -I {} echo {}
如果您不想使用 '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 上测试
这在 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"
}
seq $END
就足够了,因为默认设置是从 1 开始。从man seq
:“如果省略 FIRST 或 INCREMENT,则默认为 1”。