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.
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
- The variable trap
- C-style loops, for anything computed
- seq, and when it is the right tool
- Choosing, and a note on parallelism
- How this fits the rest of the stack
- FAQ
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
- Fixed literal range: brace expansion, {1..10}. Most readable.
- Any bound is a variable: C-style, for (( i = 1; i <= n; i++ )).
- Floating point steps, or a script that must run under sh: seq.
- 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:
- Looping in Bash: for, while, until, and the Loop That Eats Your Filenames
- Bash Append to File:
>>,tee -a, and Heredoc - How to Run .sh File in Linux: bash, chmod, and shebang
- Services on RunxBuild
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.