for i in {1..10}; do
echo "$i"
done
That's the whole answer. Bash expands {1..10} into the literal numbers 1 through 10 before the loop ever runs, then assigns each one to i in turn.
The rest of this page covers the variations you'll actually need: running it on one line, using a different range, stepping by more than 1, counting backwards, and — the part that trips up almost everyone — why this doesn't work the way you'd expect once the range boundaries are variables.
| Task | Syntax |
|---|---|
| 1 to 10 | {1..10} |
| 10 to 1 | {10..1} |
| Even numbers, 2 to 10 | {2..10..2} |
| Variable start/end | for ((i = start; i <= end; i++)) |
| One-liner | for i in {1..10}; do echo "$i"; done |
How {1..10} Actually Works (Brace Expansion)
{1..10} isn't loop syntax — it's brace expansion, a general Bash feature that generates strings before a command even runs. Per the GNU Bash Reference Manual, a sequence expression takes the form {x..y[..incr]}, where x and y are integers or letters. Bash expands it to every value from x to y inclusive, and hands the loop a plain list of words to iterate over — 1 2 3 4 5 6 7 8 9 10.
That's the same mechanism behind mkdir {a,b,c} or file{1,2,3}.txt — it just happens to also support numeric and alphabetic ranges. The loop itself is an ordinary for i in ...; do ... done; all the range logic lives entirely in the expansion, not in the loop keyword.
Run It on One Line
Same loop, written for a terminal prompt instead of a script file — semicolons replace the newlines:
for i in {1..10}; do echo "$i"; done
Output is identical either way:
1
2
3
4
5
6
7
8
9
10
Saving it as a script file instead of typing it into the terminal each time? Our guide on how to run a .sh file in Linux covers both ways to execute it once it's written.
Loop Over a Different Range
Change the two numbers inside the braces — the loop body doesn't need to change at all:
for i in {5..15}; do
echo "$i"
done
Need zero-padded output, like 01 through 10? Prefix the start value with a leading zero. Per the Bash manual, when either endpoint begins with a zero, Bash forces every generated term to the same width:
for i in {01..10}; do
echo "$i"
done
# 01
# 02
# ...
# 10
Loop With a Step (Increment)
Add a third value to the sequence expression to skip numbers. This example prints even numbers from 0 to 10:
for i in {0..10..2}; do
echo "$i"
done
# 0
# 2
# 4
# 6
# 8
# 10
The increment is always a positive integer — Bash figures out the direction from whether x is less than or greater than y.
Count Backwards
Reverse the start and end values — no extra flag or increment needed:
for i in {10..1}; do
echo "$i"
done
Per the GNU manual, the default increment is 1 or -1 "as appropriate" — Bash picks the direction automatically based on which endpoint is larger, so {10..1} counts down without you writing ..-1 anywhere.
Using Variables for the Start and End Values
This is where most guides get vague, and it's worth being precise about. The obvious-looking approach doesn't work:
start=1
end=10
for i in {$start..$end}; do
echo "$i"
done
# prints: {1..10}
# (literally — one iteration, not ten)
Why: Bash performs expansions in a fixed order, and brace expansion happens before parameter expansion — before $start and $end are resolved into their values. At the moment Bash evaluates the brace expression, it doesn't see the numbers 1 and 10 yet; it sees the literal text $start..$end. That's not a valid sequence expression — the manual is explicit that a sequence expression requires two integers or two letters, not variable references — so Bash leaves the whole thing unchanged and treats {$start..$end} as one ordinary, unexpanded string. Your loop runs exactly once, with i set to that literal text.
Brace expansion simply isn't dynamic. It's resolved once, textually, before the shell knows what any variable is about to contain.
The fix: a C-style loop
When the range needs to come from a variable, switch to Bash's C-style for loop, which evaluates its bounds as arithmetic on every iteration instead of expanding a fixed string up front:
start=1
end=10
for ((i = start; i <= end; i++)); do
echo "$i"
done
Inside (( )), Bash is in arithmetic-evaluation context, so bare variable names like start and end resolve to their values directly — you don't need the $ prefix (though $start also works there; it's just redundant). This is the correct, idiomatic way to loop over a range whose bounds aren't known until runtime.
C-Style Bash for Loop
The general form, independent of variables, mirrors C/JavaScript/PHP-style for loops:
for ((i = 1; i <= 10; i++)); do
echo "$i"
done
Three semicolon-separated clauses: an initializer, a continuation condition, and an update expression — evaluated on every pass. It's more verbose than {1..10} for a fixed range, but it's the right tool whenever the bounds are computed, whenever you need a non-1 step that itself depends on a variable, or whenever you're incrementing/decrementing by something other than a literal number.
Using seq
seq is another way to generate the same sequence:
for i in $(seq 1 10); do
echo "$i"
done
seq is a separate program (part of GNU coreutils on typical Linux systems), not a Bash builtin or a shell expansion — which means running this loop forks an external process and a command substitution just to build the list, where {1..10} or a C-style loop does the same job inside the shell itself. That makes it very slightly slower and an unnecessary dependency for a simple fixed range.
Where seq earns its place is when the numbers you're looping over come from somewhere dynamic that seq already produces well — piped into another command, or when you specifically want seq's own formatting flags, like -w for automatic zero-padding, or a non-integer step by passing it as the middle argument: seq 1 0.5 3. For a plain 1-to-10 loop inside a Bash script, though, brace expansion or the C-style loop is the more direct choice — reach for seq when you have a specific reason to, not as the default.
Common Mistakes
- 1*Variables inside
{}.*{$start..$end}doesn't expand dynamically — see the variables section above. Use a C-style loop instead. - 2*Quoting the whole expression.*
"{1..10}"disables brace expansion entirely — the quotes make it a single literal string, so the loop runs once withiset to the literal text{1..10}. Leave the braces unquoted. - 3*Running the script with
shinstead ofbash.* Brace expansion and thefor ((...))arithmetic loop are Bash extensions — they're not part of the POSIX shell language. If a script starts with#!/bin/sh, or gets run explicitly assh script.sh, both features can silently fail or behave differently depending on which shellshactually points to. Use#!/bin/bashand run it withbashor./script.shwhen you're relying on this syntax. - 4*Spaces inside the braces.*
{1 .. 10}is not valid — the sequence expression has to be written with no spaces around the... - 5*Off-by-one errors in C-style loops.*
i < 10stops after 9, not 10. Usei <= 10if you want the loop to include 10.
Quick Command Reference
| What you want | Command |
|---|---|
| 1 to 10 | for i in {1..10}; do echo "$i"; done |
| 10 down to 1 | for i in {10..1}; do echo "$i"; done |
| Custom range | for i in {5..15}; do echo "$i"; done |
| Step by 2 | for i in {0..10..2}; do echo "$i"; done |
| Zero-padded | for i in {01..10}; do echo "$i"; done |
| Variable start/end | for ((i=start; i<=end; i++)); do echo "$i"; done |
| Using seq | for i in $(seq 1 10); do echo "$i"; done |
Conclusion
For a fixed range you can write out by hand, {1..10} is the simplest option — no external command, no arithmetic syntax, just a brace expansion that hands the loop a ready-made list. The moment either boundary needs to come from a variable, switch to the C-style for ((i = start; i <= end; i++)) loop instead — brace expansion is resolved as plain text before your variables are ever read, so it can't do that job. Keep seq in reserve for when you actually need its formatting options or you're piping numbers into something else, not as the default way to count to ten.
Originally published on SSHFlow.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.