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

后端 未结 20 1866
予麋鹿
予麋鹿 2020-11-21 05:19

How do I iterate over a range of numbers in Bash when the range is given by a variable?

I know I can do this (called \"sequence expression\" in the Bash documentatio

相关标签:
20条回答
  • 2020-11-21 05:40

    Here is why the original expression didn't work.

    From man bash:

    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.

    So, brace expansion is something done early as a purely textual macro operation, before parameter expansion.

    Shells are highly optimized hybrids between macro processors and more formal programming languages. In order to optimize the typical use cases, the language is made rather more complex and some limitations are accepted.

    Recommendation

    I would suggest sticking with Posix1 features. This means using for i in <list>; do, if the list is already known, otherwise, use while or seq, as in:

    #!/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 is a great shell and I use it interactively, but I don't put bash-isms into my scripts. Scripts might need a faster shell, a more secure one, a more embedded-style one. They might need to run on whatever is installed as /bin/sh, and then there are all the usual pro-standards arguments. Remember shellshock, aka bashdoor?

    0 讨论(0)
  • 2020-11-21 05:40

    Replace {} with (( )):

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

    Yields:

    0
    1
    2
    3
    4
    
    0 讨论(0)
  • 2020-11-21 05:41

    I've combined a few of the ideas here and measured performance.

    TL;DR Takeaways:

    1. seq and {..} are really fast
    2. for and while loops are slow
    3. $( ) is slow
    4. for (( ; ; )) loops are slower
    5. $(( )) is even slower
    6. Worrying about N numbers in memory (seq or {..}) is silly (at least up to 1 million.)

    These are not conclusions. You would have to look at the C code behind each of these to draw conclusions. This is more about how we tend to use each of these mechanisms for looping over code. Most single operations are close enough to being the same speed that it's not going to matter in most cases. But a mechanism like for (( i=1; i<=1000000; i++ )) is many operations as you can visually see. It is also many more operations per loop than you get from for i in $(seq 1 1000000). And that may not be obvious to you, which is why doing tests like this is valuable.

    Demos

    # 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
    
    0 讨论(0)
  • 2020-11-21 05:42

    if you don't wanna use 'seq' or 'eval' or jot or arithmetic expansion format eg. for ((i=1;i<=END;i++)), or other loops eg. while, and you don't wanna 'printf' and happy to 'echo' only, then this simple workaround might fit your budget:

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

    PS: My bash doesn't have 'seq' command anyway.

    Tested on Mac OSX 10.6.8, Bash 3.2.48

    0 讨论(0)
  • 2020-11-21 05:43

    This is another way:

    end=5
    for i in $(bash -c "echo {1..${end}}"); do echo $i; done
    
    0 讨论(0)
  • 2020-11-21 05:43

    If you want to stay as close as possible to the brace-expression syntax, try out the range function from bash-tricks' range.bash.

    For example, all of the following will do the exact same thing as echo {1..10}:

    source range.bash
    one=1
    ten=10
    
    range {$one..$ten}
    range $one $ten
    range {1..$ten}
    range {1..10}
    

    It tries to support the native bash syntax with as few "gotchas" as possible: not only are variables supported, but the often-undesirable behavior of invalid ranges being supplied as strings (e.g. for i in {1..a}; do echo $i; done) is prevented as well.

    The other answers will work in most cases, but they all have at least one of the following drawbacks:

    • Many of them use subshells, which can harm performance and may not be possible on some systems.
    • Many of them rely on external programs. Even seq is a binary which must be installed to be used, must be loaded by bash, and must contain the program you expect, for it to work in this case. Ubiquitous or not, that's a lot more to rely on than just the Bash language itself.
    • Solutions that do use only native Bash functionality, like @ephemient's, will not work on alphabetic ranges, like {a..z}; brace expansion will. The question was about ranges of numbers, though, so this is a quibble.
    • Most of them aren't visually similar to the {1..10} brace-expanded range syntax, so programs that use both may be a tiny bit harder to read.
    • @bobbogo's answer uses some of the familiar syntax, but does something unexpected if the $END variable is not a valid range "bookend" for the other side of the range. If END=a, for example, an error will not occur and the verbatim value {1..a} will be echoed. This is the default behavior of Bash, as well--it is just often unexpected.

    Disclaimer: I am the author of the linked code.

    0 讨论(0)
提交回复
热议问题