关于shell:如何迭代bash中变量定义的一系列数字?

How do I iterate over a range of numbers defined by variables in Bash?

当范围由变量给出时,如何在bash中迭代一个数字范围?

我知道我可以这样做(在bash文档中称为"序列表达式"):

1
 for i in {1..5}; do echo $i; done

它给出:

1
2
3
4
5

但是,如何用变量替换任意一个范围端点?这不起作用:

1
2
END=5
for i in {1..$END}; do echo $i; done

哪些印刷品:

{1..5}


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

编辑:我比其他方法更喜欢seq,因为我实际上能记住它;)


seq方法是最简单的,但是bash有内置的算术评估。

1
2
3
4
5
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将它们视为算术。


讨论

如加亚罗所建议的,使用seq是可以的。pax diablo建议使用bash循环来避免调用子进程,如果$end太大,那么它还有一个额外的优点,那就是对内存更友好。Zathrus在循环实现中发现了一个典型的错误,并暗示由于i是一个文本变量,因此连续的往返数字转换将以相应的速度进行。

整数算术

这是bash循环的改进版本:

1
2
3
4
5
6
7
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的整个手册页(就像我读过KornShell(ksh手册页,那是很久以前的事了),所以我错过了。

所以,

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

似乎是内存效率最高的方法(不需要分配内存来消耗seq的输出,如果端非常大,这可能是一个问题),尽管可能不是"最快的"。

最初的问题

Eschercycle注意到,a..b bash符号仅适用于文本;与bash手册相应,为真。一个人可以通过一个没有exec()的单个(内部)fork()来克服这个障碍(就像调用seq的情况一样,这是另一个需要fork+exec的图像):

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

evalecho都是bash内置的,但是命令替换($(…)构造)需要fork()


这就是为什么最初的表达方式不起作用。

从人巴什:

Brace expansion is performed before
any other expansions, and any
characters special to other
expansions are preserved in the
result. It is strictly textual. Bash
does not apply any syntactic
interpretation to the context of
the expansion or the text between the
braces.

所以,在参数展开之前,大括号展开是一个纯文本的宏操作。

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

Recommendation

我建议继续使用posix1特性。这意味着使用for i in ; do,如果列表已知,则使用whileseq,如:

1
2
3
4
5
6
7
8
9
10
11
12
#!/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的设备上运行,然后就有了所有常见的pro-standards参数。还记得Shellshock,又名bashdoor吗?


POSIX方式

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

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

输出:

1
2
3
4
2
3
4
5

不是posix的东西:

  • 没有美元的(( )),尽管这是posix本身提到的一个常见扩展。
  • [[。这里就足够了。另请参见:bash中的单方括号和双方括号有什么区别?
  • for ((;;))
  • seq(GNU核心用户)
  • {start..end},不能使用bash手册中提到的变量。
  • let i=i+1:位置7 2。shell命令语言不包含单词let,在bash --posix4.3.42上失败。
  • 可能需要在EDOCX1[9]兑换美元,但我不确定。posix 7 2.6.4算术展开表示:


    If the shell variable x contains a value that forms a valid integer constant, optionally including a leading plus or minus sign, then the arithmetic expansions"$((x))" and"$(($x))" shall return the same value.

    但从字面上看,这并不意味着$((x+1))会扩大,因为x+1不是一个变量。


另一层间接性:

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


你可以使用

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


如果您在bsd/os x上,可以使用jot而不是seq:

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


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

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

这将产生

1
2
3
4
5
6
07
08
09
10
11
12


这在bash中很有效:

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


我知道这个问题是关于bash的,但就记录而言,ksh93更聪明,并能按预期执行:

1
2
3
4
5
6
7
8
9
10
11
$ 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}


这是另一种方式:

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


如果您希望尽可能接近大括号表达式语法,请尝试bash技巧的range.bash中的range函数。

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

1
2
3
4
5
6
7
8
source range.bash
one=1
ten=10

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

它试图用尽可能少的"gotchas"来支持本机bash语法:不仅支持变量,而且还防止了作为字符串(例如for i in {1..a}; do echo $i; done)提供的无效范围的不良行为。

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

  • 它们中的许多使用子shell,这可能会损害性能,在某些系统上可能是不可能的。
  • 它们中的许多依赖于外部程序。即使是seq也是一个二进制文件,必须安装才能使用,必须由bash加载,并且必须包含您期望的程序,以便在这种情况下工作。无论无处不在与否,这比bash语言本身更值得依赖。
  • 只使用本机bash功能的解决方案,如@ephemient,将无法在字母范围内工作,如{a..z};括号扩展将起作用。不过,问题是关于数字的范围,所以这是个骗局。
  • 它们中的大多数在视觉上与{1..10}大括号扩展范围语法不同,因此使用这两种语法的程序可能有一点难以阅读。
  • @Bobbogo的回答使用了一些熟悉的语法,但如果$END变量不是范围另一端的有效范围"bookend",则会发生意外。例如,如果END=a,则不会发生错误,并且将回送逐字值{1..a}。这也是bash的默认行为——这通常是出乎意料的。

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


这些都很好,但seq被认为是不推荐使用的,大多数只适用于数值范围。

如果您将for循环用双引号括起来,那么当您回送字符串时,起始变量和结束变量将被取消引用,并且您可以将字符串直接送回bash执行。$i需要用的进行转义,因此在发送到子shell之前不进行评估。

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

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

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

它应该生成的唯一"开销"应该是bash的第二个实例,因此它应该适合于密集的操作。


(( ))替换{}

1
2
3
4
5
6
tmpstart=0;
tmpend=4;

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

产量:

1
2
3
4
5
0
1
2
3
4


如果您正在执行shell命令,并且您(像我一样)对管道有一种迷恋,那么这个命令很好:

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


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

Takeways博士:

  • seq{..}真的很快
  • forwhile循环很慢。
  • $( )
  • for (( ; ; ))环速度较慢
  • $(( ))更慢
  • 担心内存中的n个数字(seq或…)是愚蠢的(至少100万)。
  • 这些不是结论。为了得出结论,您必须查看每个代码后面的C代码。这更多的是关于我们如何使用这些机制中的每一个来循环代码。大多数单次操作的速度都很接近,在大多数情况下都不重要。但是像for (( i=1; i<=1000000; i++ ))这样的机制是许多操作,您可以从视觉上看到。每个循环的操作也比从for i in $(seq 1 1000000)获得的要多。这对你来说可能并不明显,这就是为什么做这样的测试是有价值的。

    演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    # 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


    这在bash和korn中有效,也可以从高到低的数字。可能不是最快或最漂亮,但效果很好。也可以处理底片。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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"
    }