Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Bash For Loop Over a Range: Brace Expansion, seq, and the Variable Trap

Sean

Platform Writer

Aug 14, 2026
7 min read

For a fixed range, use brace expansion: for i in {1..10}. When either bound is a variable, brace expansion does not work and you need the C-style form: for ((i = 1; i <= n; i++)). That second sentence is the entire reason this trips people up, because the broken version produces no error at all.

Bash For Loop Over a Range: Brace Expansion, seq, and the Variable Trap

Three ways to write this loop exist and they are not interchangeable. Knowing which one handles variables, which handles steps, and which is portable to plain sh saves you from a bug that prints a literal brace and looks like a typo.

Table of contents

Brace expansion, for ranges known when you write the script

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

for i in {0..20..5}; do    # step of 5: 0 5 10 15 20
  echo "$i"
done

for i in {10..1}; do       # counts down
  echo "$i"
done

for c in {a..e}; do        # characters work too
  echo "$c"
done

for i in {01..10}; do      # zero padded: 01 02 ... 10
  echo "$i"
done

This is the most readable form and the right default when the numbers are literals. The step syntax requires bash 4 or later, which is everything except the ancient bash that ships with macOS.

One thing worth understanding about how it works: brace expansion happens very early, before variables are substituted, and it expands to the full list of values in memory. So {1..1000000} builds a million-element list before the loop starts. For very large ranges the C-style loop is better behaved.

The variable trap

This is the failure the whole post exists for.

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

# Prints, exactly once:
# {1..5}

Brace expansion runs before parameter expansion. When bash processes the braces, $n is still literally $n, which is not a number, so the construct is not a valid range and bash leaves the text alone. The loop then runs once over the single string {1..5}.

No error, no warning. The script runs, does a fraction of the work, and exits successfully. In a deploy script or a batch job this can go unnoticed for a long time.

The fix is the C-style loop, which evaluates its expressions at runtime.

n=5
for (( i = 1; i <= n; i++ )); do
  echo "$i"
done

Note there are no dollar signs on the variables inside the double parentheses. Arithmetic context resolves names automatically, and $i works too but is unnecessary.

You will see eval suggested as a workaround for the brace form. Do not use it. It re-parses the string as code, so a variable containing something unexpected becomes a command injection, and the C-style loop is both safer and clearer.

C-style loops, for anything computed

for (( i = 0; i < 10; i++ )); do :; done          # zero based
for (( i = 10; i > 0; i-- )); do :; done          # counting down
for (( i = 0; i < 100; i += 5 )); do :; done      # arbitrary step
for (( i = 1; i <= end; i++ )); do :; done        # variable bound
for (( i = start; i <= end; i += step )); do :; done   # all variables

# Two counters at once.
for (( i = 0, j = 10; i < j; i++, j-- )); do
  echo "$i $j"
done

This form handles every case brace expansion cannot: variable bounds, computed steps, and conditions more complex than a simple range. It also does not build the list up front, so a loop to ten million costs nothing in memory.

The trade is readability. For a fixed small range, {1..10} communicates intent better than the three-clause version, so use braces where they work and this where they do not.

seq, and when it is the right tool

for i in $(seq 1 10); do echo "$i"; done
for i in $(seq 0 5 20); do echo "$i"; done      # start, step, end
for i in $(seq 1 "$n"); do echo "$i"; done      # variables are fine here

seq -w 1 10        # zero padded to equal width
seq -s ',' 1 5     # custom separator: 1,2,3,4,5
seq 1 0.5 3        # floating point steps

seq is an external command rather than shell syntax, which gives it both its advantages and its drawbacks. It handles variables without ceremony, it does floating point, and it works in plain sh where brace ranges and C-style loops do not.

The drawbacks: it spawns a process on every loop, the output goes through word splitting so a strange IFS can break it, and it is not in the POSIX standard, so a minimal container image may not have it.

Use seq when you need floating point, or when the script must run under sh. Otherwise prefer the shell built-ins.

For POSIX sh with no seq available, the while loop is the portable fallback.

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

Choosing, and a note on parallelism

  1. Fixed literal range: brace expansion, {1..10}. Most readable.
  2. Any bound is a variable: C-style, for (( i = 1; i <= n; i++ )).
  3. Floating point steps, or a script that must run under sh: seq.
  4. Very large ranges: C-style, since brace expansion materialises the whole list.

One practical addition. When the loop body does real work rather than arithmetic, running iterations sequentially is often the actual bottleneck. GNU parallel or xargs will run them concurrently with a controlled degree of parallelism.

# Four at a time, rather than one after another.
seq 1 100 | xargs -P 4 -I {} ./process.sh {}

Worth knowing before you spend an afternoon optimising the loop syntax when the loop was never the slow part.

How this fits the rest of the stack

Loops like these usually live in a build script or a batch job, and the difficulty is rarely the syntax. It is that nobody can see whether last night’s run finished or died at iteration forty. Running the job somewhere with logs attached to each execution turns that from an investigation into a glance. The RunxBuild hosting calculator shows the service, database, and storage as separate line items, so a scheduled worker can be costed on its own.

Useful related references:

FAQ

Why does {1..$n} not work in bash?

Brace expansion happens before parameter expansion, so when the braces are processed the variable is still literal text and the range is invalid. Bash leaves it alone and the loop runs once over the string. Use a C-style loop instead.

How do I loop with a step in bash?

Brace expansion supports {start..end..step}, as in {0..20..5}, on bash 4 and later. A C-style loop with i += step works everywhere and accepts variables for all three values.

Is seq or brace expansion better?

Brace expansion is faster because it is shell syntax rather than an external process, and it is the right default. Use seq for floating point steps or when the script must run under plain sh.

How do I loop a variable number of times in bash?

Use the C-style form: for (( i = 1; i <= n; i++ )). It evaluates its expressions at runtime, so variables work. Do not reach for eval to force the brace form to work.

How do I count down in a bash for loop?

Reverse the brace range as {10..1}, or use a C-style loop with i— and a greater-than condition.

#Bash For Loop#Brace Expansion#Bash Range#Shell Scripting#Linux