Advertisement
Advertisement


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


Question

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 documentation):

 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}

2018/09/01
1
1627
9/1/2018 7:04:56 PM

Accepted Answer

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

edit: I prefer seq over the other methods because I can actually remember it ;)

2016/04/23
1817
4/23/2016 12:41:06 AM

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;expr3)); construct works just like for (expr1;expr2;expr3) in C and similar languages, and like other ((expr)) cases, Bash treats them as arithmetic.

2014/09/03

discussion

Using seq is fine, as Jiaaro 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 Korn shell (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 most memory-efficient way (it won't be necessary to allocate memory to consume seq's output, which could be a problem if END is very large), although probably not the “fastest”.

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).

2017/05/23

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?

2017/02/13

The POSIX way

If you care about portability, use the example from the POSIX standard:

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

Output:

2
3
4
5

Things which are not POSIX:


Another layer of indirection:

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

Source: https://stackoverflow.com/questions/169511
Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Email: [email protected]