Sunday, May 20, 2012

How do I iterate over a range of numbers in bash?


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:




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



Which gives:




1

2

3

4

5




Yet how can I replace either of the range endpoints with a variable? This doesn't work:




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



Which prints:




{1..5}



Source: Tips4all

11 comments:

  1. for i in `seq 1 $END`; do echo $i; done

    ReplyDelete
  2. discussion

    Using seq is fine, as Jim Robert suggested. Pax Diablo suggested a bash loop to avoid calling a subprocess, with the additional advantage of being more memory friendly if $END is too large. Zathrus spotted a typical bug in the loop implementation, and also hinted that since i is a text variable, continuous conversions to-and-fro numbers are performed with an associated slow-down.

    integer arithmetic

    This is an improved version of the bash loop:

    typeset -i i END
    let END=5 i=1
    while ((i<=END)); do
    echo $i

    let i++
    done


    If the only thing that we want is the echo, then we could write echo $((i++)).

    ephemient taught me something: bash allows for ((expr;expr;expr)) constructs. Since I've never read the whole man page for bash (like I've done with the ksh man page, and that was a long time ago), I missed that.

    So,

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


    seems to be the cleanest way, and possibly the "fastest"; it sure won't be necessary to allocate memory to consume seq's output, which could be a problem if END is very large.

    the initial question

    eschercycle noted that the {a..b} bash notation works only with literals; true, accordingly to the bash manual. One can overcome this obstacle with a single (internal) fork() without an exec() (as is the case with calling seq, which being another image requires a fork+exec):

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


    Both eval and echo are bash builtins, but a fork() is required for the command substitution (the $(…) construct).

    ReplyDelete
  3. The seq method is the simplest, but Bash has built-in arithmetic evaluation.

    END=5
    for ((i=1;i<=END;i++)); do
    echo $i
    done
    # ==> outputs 1 2 3 4 5 on separate lines


    The "for ((expr1;expr2;expr2))" construct works just like "for (expr1;expr2;expr3)" in C and similar languages, and like other ((expr)) cases, Bash treats them as arithmetic.

    ReplyDelete
  4. You can use

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

    ReplyDelete
  5. If you're on BSD / OS X you can use jot instead of seq:

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

    ReplyDelete
  6. This works for me in bash:

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

    ReplyDelete
  7. Since the "how to" part of the question has been completely answered by now, I will comment on 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.

    ReplyDelete
  8. Another layer of indirection:

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

    ReplyDelete
  9. My version of bash doesn't seem to support the curly brace notation at all.

    Can you do this?

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


    Update: I found a bash that supports the curly braces; the above does not work.

    ReplyDelete
  10. These are all nice but seq is supposedly deprecated and most only work with numeric ranges.

    If you enclose your for loop in double quotes, the start and end variables will be dereferenced when you echo the string, and you can ship the string right back to BASH for execution. $i needs to be escaped with \'s so it is NOT evaluated before being sent to the subshell.

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


    This output can also be assigned to a variable:

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


    The only "overhead" this should generate should be the second instance of bash so it should be suitable for intensive operations.

    ReplyDelete
  11. You would still use seq in cases where you needed to generate sequences of real numbers. The
    Bash for loop only supports integers.

    ReplyDelete