ChatGPT解决这个技术问题 Extra ChatGPT

如何迭代 Bash 中变量定义的一系列数字?

当范围由变量给出时,如何迭代 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}

大家好,我在这里阅读的信息和提示都非常有帮助。我认为最好避免使用 seq。原因是某些脚本需要可移植,并且必须在各种 unix 系统上运行,其中某些命令可能不存在。举个例子,在 FreeBSD 系统上默认不存在 seq。
我不记得究竟是哪个版本的 Bash,但这个命令也支持尾随零。这有时真的很有帮助。命令 for i in {01..10}; do echo $i; done 会给出类似 01, 02, 03, ..., 10 的数字。
对于像我这样只想遍历 array 的索引范围的人来说,bash 方式是:myarray=('a' 'b' 'c'); for i in ${!myarray[@]}; do echo $i; done(注意感叹号)。它比原始问题更具体,但可能会有所帮助。请参阅bash parameter expansions
大括号扩展也用于像 {jpg,png,gif} 这样的表达式,这里没有直接解决,尽管答案是相同的。请参阅Brace expansion with variable? [duplicate],它被标记为与此重复。

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

编辑:我更喜欢 seq 而不是其他方法,因为我实际上可以记住它;)


seq 涉及执行通常会减慢速度的外部命令。这可能无关紧要,但如果您正在编写脚本来处理大量数据,它就变得很重要。
单线就好了。 Pax 的解决方案也很好,但如果性能真的是一个问题,我不会使用 shell 脚本。
seq 只被调用一次来生成数字。 exec()'ing 它不应该很重要,除非这个循环在另一个紧密循环中。
外部命令并不真正相关:如果您担心运行外部命令的开销,您根本不想使用 shell 脚本,但通常在 unix 上开销很低。但是,如果 END 较高,则会出现内存使用问题。
请注意,seq $END 就足够了,因为默认设置是从 1 开始。从 man seq:“如果省略 FIRST 或 INCREMENT,则默认为 1”。
e
ephemient

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 的依赖。用它!
@MarinSagovac 这确实有效,并且没有语法错误。你确定你的shell是Bash吗?
@MarinSagovac 确保将 #!/bin/bash 设为脚本的第一行。 wiki.ubuntu.com/…
只是一个非常简短的问题:为什么 ((i=1;i<=END;i++)) AND NOT ((i=1;i<=$END;i++));为什么在END之前没有$?
@Baedsch:出于同样的原因,我不用作 $i。用于算术评估的 bash 手册页状态:“在表达式中,shell 变量也可以按名称引用,而不使用参数扩展语法。”
C
Community

讨论

正如 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

evalecho 都是 Bash 内置函数,但命令替换需要 fork()$(…) 构造)。


C 风格循环的唯一缺点是它不能使用命令行参数,因为它们以“$”开头。
@karatedog:脚本中的 for ((i=$1;i<=$2;++i)); do echo $i; done 在 bash v.4.1.9 上对我来说效果很好,所以我看不到命令行参数有问题。你的意思是别的吗?
似乎 eval 解决方案比内置的 C 类更快: $ time for ((i=1;i<=100000;++i));做 :; done real 0m21.220s user 0m19.763s sys 0m1.203s $ time for i in $(eval echo "{1..100000}");做 :;完毕;真实 0m13.881s 用户 0m13.536s 系统 0m0.152s
是的,但是 eval 是邪恶的...@MarcinZaluski time for i in $(seq 100000); do :; done 快得多!
性能必须是特定于平台的,因为 eval 版本在我的机器上是最快的。
D
DigitalRoss

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

来自 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-isms 放入我的脚本中。脚本可能需要更快的 shell、更安全的 shell、更嵌入式的 shell。他们可能需要在安装为 /bin/sh 的任何东西上运行,然后有所有常见的支持标准的参数。还记得 shellshock,又名 bashdoor 吗?


我没有权力,但我会把它移到列表的前面,首先是 bash 肚脐凝视,但紧接着是 C 风格的 for 循环和算术评估。
一个暗示是,与 seq 相比,大括号扩展并没有节省太多内存。例如,echo {1..1000000} | wc 表明回显产生 1 行、100 万字和 6,888,896 字节。尝试 seq 1 1000000 | wc 会产生一百万行、一百万字和 6,888,896 字节,并且速度也快了七倍以上,如 time 命令所测量的那样。
注意:我之前在回答中提到过 POSIX while 方法:stackoverflow.com/a/31365662/895245 但很高兴您同意 :-)
我已将此答案包含在下面的性能比较答案中。 stackoverflow.com/a/54770805/117471(这是给我自己的一张便条,用来记录我还有哪些事情要做。)
@mateor我认为C风格的循环和算术评估是相同的解决方案。我错过了什么吗?
C
Community

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 不是变量。


刚刚对这个答案投了 4 票,这是非常不寻常的。如果这是在某个链接聚合网站上发布的,请给我一个链接,干杯。
引号指的是 x,而不是整个表达式。 $((x + 1)) 很好。
虽然不可移植,并且与 GNU seq 不同(BSD seq 允许您使用 -t 设置序列终止字符串),但 FreeBSD 和 NetBSD 从 9.0 和 3.0 开始也分别具有 seq
@CiroSantilli @chepner $((x+1))$((x + 1)) 解析完全相同,因为当解析器标记 x+1 时,它将分为 3 个标记:x+1x 不是有效的数字标记,但它是有效的变量名标记,但 x+ 不是,因此拆分。 + 是一个有效的算术运算符记号,但 +1 不是,所以记号在此处再次被拆分。等等。
我已将此答案包含在下面的性能比较答案中。 stackoverflow.com/a/54770805/117471(这是给我自己的一张便条,用来记录我还有哪些事情要做。)
P
Peter Hoffmann

您可以使用

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

seq 涉及执行通常会减慢速度的外部命令。
它不涉及为每次迭代执行外部命令,只需执行一次。如果启动一个外部命令的时间有问题,则说明您使用了错误的语言。
那么嵌套是唯一重要的情况吗?我想知道是否存在性能差异或一些未知的技术副作用?
@Squeaky 这是一个单独的问题,在这里回答:stackoverflow.com/questions/4708549/…
我已将此答案包含在下面的性能比较答案中。 stackoverflow.com/a/54770805/117471(这是给我自己的一张便条,用来记录我还有哪些事情要做。)
b
bobbogo

另一层间接:

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

+1:另外,eval 'for i in {1..'$END'}; do ... ' eval 似乎是解决这个问题的自然方法。
h
hossbear

如果你需要它的前缀,那么你可能会喜欢这个

 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/" 更容易吗?
B
Bruno Bronosky

我在这里结合了一些想法并测量了性能。

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、算术比较、增量等。这有什么令人惊讶的吗?谁在乎循环用具完成它的工作需要多长时间——运行时很可能由循环的内容主导。
@bobbogo 你是对的,这真的是关于操作计数。我更新了我的答案以反映这一点。我们进行的许多调用实际上执行的操作比我们预期的要多。我从我运行的大约 50 个测试的列表中缩小了范围。我预计即使对于这群人来说,我的研究也太书呆子了。与往常一样,我建议您优先考虑您的编码工作,如下所示:让它更短;使其可读;让它更快;使其便携。通常 #1 会导致 #3。除非必要,否则不要在#4 上浪费时间。
这是一个有趣的练习,尽管最初的问题是关于使用变量控制迭代,例如 {..} 不允许。
我从来不知道 {i..n} !太酷了,每种语言都应该有这个。
j
jefeveizen

如果你在 BSD / OS X 上,你可以使用 jot 而不是 seq:

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

macOS 有 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.
p
paxdiablo

这在 bash 中运行良好:

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

echo $((i++)) 起作用并将其合并到一行中。
这有不需要的 bash 扩展。 POSIX 版本:stackoverflow.com/a/31365662/895245
@Ciro,因为这个问题特别说明了 bash,并且有一个 bash 标签,我想你可能会发现 bash 的“扩展”还不错:-)
@paxdiablo我并不是说它不正确,但是为什么我们可以不便携;-)
bash 中,我们可以简单地执行 while [[ i++ -le "$END" ]]; do 来执行测试中的(后)增量
t
theBuzzyCoder

有很多方法可以做到这一点,但我更喜欢的方法如下

使用序列

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

A
Adrian Frühwirth

我知道这个问题是关于 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}

J
Jahid

这是另一种方式:

end=5
for i in $(bash -c "echo {1..${end}}"); do echo $i; done

这具有生成另一个 shell 的开销。
实际上,这是非常可怕的,因为它会在 1 个足够的情况下生成 2 个炮弹。
Z
Zac B

如果您想尽可能接近大括号表达式语法,请尝试 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 的默认行为——只是经常出乎意料。

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


s
sjngm

这些都很好,但 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 的第二个实例,因此它应该适合密集型操作。


s
sth

{} 替换为 (( ))

tmpstart=0;
tmpend=4;

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

产量:

0
1
2
3
4

我已将此答案包含在下面的性能比较答案中。 stackoverflow.com/a/54770805/117471(这是给我自己的一张便条,用来记录我还有哪些事情要做。)
A
Alex Spangher

如果您正在执行 shell 命令,并且您(像我一样)喜欢流水线,那么这个很好:

seq 1 $END | xargs -I {} echo {}


Z
Zimba

如果您不想使用 '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 上测试


E
Ethan Post

这在 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"
}