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
for i in `seq 1 $END`; do echo $i; done
ReplyDeletediscussion
ReplyDeleteUsing 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).
The seq method is the simplest, but Bash has built-in arithmetic evaluation.
ReplyDeleteEND=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.
You can use
ReplyDeletefor i in $(seq $END); do echo $i; done
If you're on BSD / OS X you can use jot instead of seq:
ReplyDeletefor i in $(jot $END); do echo $i; done
This works for me in bash:
ReplyDeleteEND=5
i=1 ; while [[ $i -le $END ]] ; do
echo $i
((i = i + 1))
done
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.
ReplyDeleteFrom 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.
Another layer of indirection:
ReplyDeletefor i in $(eval echo {1..$END}); do
∶
My version of bash doesn't seem to support the curly brace notation at all.
ReplyDeleteCan 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.
These are all nice but seq is supposedly deprecated and most only work with numeric ranges.
ReplyDeleteIf 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.
You would still use seq in cases where you needed to generate sequences of real numbers. The
ReplyDeleteBash for loop only supports integers.