In a bash script, `n=5; for i in {1..$n}; do echo "$i"; done` prints `{1..5}` instead of the numbers 1 through 5. Why does that happen, and which loop forms do iterate a variable number of times?
answer
- expansion order decides the outcome
- braces run before the dollar sign
- a range needs literal numbers
- evaluate per iteration, not once
- (( )) takes variables happily
basics
~20 sBrace expansion runs before parameter expansion, so bash sees {1..$n}, does not recognise it as a numeric range, and leaves it as text that later becomes {1..5}. Use a C-style loop, for ((i=1; i<=n; i++)), or iterate over seq output.
solid answer
~50 sBrace expansion is the first expansion bash performs on a word, and at that moment `$n` is still the literal characters `$n`. `{1..$n}` is not a valid numeric range, so bash leaves the brace text alone; parameter expansion then runs and replaces `$n` with 5, producing the single word `{1..5}`, which is what the loop iterates over. Braces only build ranges from literal numbers known at parse time. For a count held in a variable, use the C-style form: `for ((i=1; i<=n; i++)); do ...; done` — it is a bash and ksh extension, not POSIX, and it evaluates its expressions on every iteration, so a variable bound works. The portable alternative is `for i in $(seq 1 "$n")`, which shells out to an external command, or a `while` loop with an arithmetic condition. Reaching for `eval` to force the braces to re-expand is the wrong answer.
code
bash · 7 linesn=4
echo '--- brace range with a variable ---'
for i in {1..$n}; do echo "[$i]"; done
echo '--- C-style ---'
for ((i = 1; i <= n; i++)); do echo "[$i]"; donego deeper
Know that brace ranges only work with literal numbers, and that a count stored in a variable calls for for ((i=1; i<=n; i++)) or seq. Recognise {1..5} printed as text as the symptom.
Explain the ordering: brace expansion precedes parameter expansion, so $n is not yet a number when the range is considered. Contrast a list built once with arithmetic expressions evaluated per iteration.
Choose deliberately in context — C-style for a bash script, a POSIX while when the shebang is /bin/sh, seq when you need padding or a step — and reject eval on injection grounds even when the value looks trusted today.
Own the portability policy: which shell your scripts declare, whether bash-4 features like {0..20..5} are permitted given hosts still on bash 3.2, and how that decision is enforced rather than left to each author's habit.
## Why the braces do not expand Bash expands a word in a fixed order, and brace expansion comes first — before parameter expansion, command substitution, arithmetic expansion, word splitting and pathname expansion. When bash looks at `{1..$n}` it is deciding whether the braces contain a valid sequence, and all it can see there is `1..$n`. `$n` is not a number at that point (the expansion that would turn it into one has not run yet), so the construct is not a range, and bash leaves the whole word untouched. Parameter expansion then does its job and turns the surviving text into `{1..5}` — a single ordinary word, which the `for` loop faithfully iterates over exactly once. The same explanation covers why `{1..5}` with literal numbers works fine: everything the expansion needs is present at the time it runs. ```bash echo {1..3} # 1 2 3 n=3; echo {1..$n} # {1..3} <- one word, not three ``` ## Form 1: the C-style loop ```bash for ((i = 1; i <= n; i++)); do echo "$i" done ``` The three parts inside `(( ))` are arithmetic expressions, evaluated once at the start, before every iteration, and after every iteration respectively. Because they are evaluated each time round rather than expanded once into a list, a variable bound is exactly what they are for. Inside an arithmetic context a bare name is read as a variable, which is why `i <= n` works without a `$` — though writing `$n` there is also legal and harmless. This form is a bash and ksh extension; it is not in POSIX, so it does not work in a `#!/bin/sh` script running under dash. Use it whenever you have decided the script is a bash script. ## Form 2: `seq` ```bash for i in $(seq 1 "$n"); do echo "$i" done ``` `seq` is an external program, not a builtin, and not specified by POSIX either — though it is present on GNU and BSD systems including macOS. It forks a process per loop, which is irrelevant for small counts and wasteful for large ones. The unquoted substitution is safe here only because the output is digits and newlines; the word-splitting hazards that make `$(ls)` dangerous do not bite on integers. `seq` earns its place when you need a step or formatting that arithmetic would make ugly, for example `seq -w 1 10` for zero-padded values. ## Form 3: `while` with an arithmetic condition ```bash i=1 while ((i <= n)); do echo "$i" i=$((i + 1)) done ``` Useful when the increment is not uniform, or when the loop variable is advanced from inside the body based on data. Note one trap that bites strict-mode scripts here: `((expr))` returns exit status 1 when the expression evaluates to zero, so `((i++))` with `i` at 0 returns a failure status that can terminate a script running under `set -e`. ## Ranges with literal bounds When the bounds really are constants, braces are the clearest option and support a step in bash 4.0 and later: `{0..20..5}` gives `0 5 10 15 20`, and `{a..e}` gives letters. Zero padding works too — `{01..12}` produces `01 02 ... 12`, which is handy for month or day directories. ## What not to do `eval "for i in {1..$n}"` does technically work, because the string is re-parsed after `$n` has been substituted, so brace expansion gets a second chance at a now-literal range. It is still the wrong answer in an interview and in review: it re-parses data as code, so if `n` ever comes from a caller or a file you have handed that caller a shell injection. There are two clean forms that need no re-parsing; use one of them.
- Why does `for ((i=1; i<=n; i++))` succeed where the brace range failed?Its three parts are arithmetic expressions re-evaluated as the loop runs, not a word list built once before the loop starts. Bash therefore reads the current value of `n` on every iteration. Brace expansion, by contrast, gets one shot at building a list and needs literal numbers at that moment.
- Is `for ((i=0; i<n; i++))` safe to use in a `#!/bin/sh` script?No. The C-style `for` is a bash and ksh extension and is not in POSIX, so it fails under dash, which is `/bin/sh` on Debian and Ubuntu. In a `#!/bin/sh` script use a `while` loop with `[ "$i" -le "$n" ]` and `i=$((i + 1))`, both of which are POSIX.
saying these in an interview costs you the question
- Says the braces need double quotes to expand
- Claims {1..$n} works but n was unset
- Reaches for eval as the recommended fix
- Thinks seq is a shell builtin
- Believes brace expansion happens last