skip to content

A bash script sets `n=5` and then runs `for i in {1..$n}; do echo $i; done`. What does that loop actually print, and why?

level: middleimportance: should knowfreq 48%

answer

  1. braces run first
  2. cannot see a value not yet expanded
  3. invalid range is left literal
  4. results are not re-scanned
  5. use an arithmetic for loop

basics

~20 s

It prints the single line {1..5}. Brace expansion runs before variables are expanded, so bash sees {1..$n}, decides it is not a valid range, and leaves it literal; the variable is substituted afterwards, producing one loop iteration over that text.

solid answer

~40 s

It loops exactly once and prints `{1..5}`. Bash performs brace expansion first, before parameter expansion, so at that moment the text is `{1..$n}` — `$n` is not an integer literal, the sequence expression is invalid, and bash leaves the braces alone as ordinary characters. Only afterwards does parameter expansion turn `$n` into `5`, and the result is never re-scanned for brace expansion, so `{1..5}` stays a single word that the `for` iterates over once. The fix is to use a construct that runs at a later stage: a C-style loop, `for ((i=1; i<=n; i++))`, is the usual choice and needs no external command; `for i in $(seq 1 "$n")` works too because command substitution happens after braces. Wrapping it in `eval` also works but buys a second parse you rarely need.

code

bash · 15 lines
bash
n=5

for i in {1..$n}; do echo "$i"; done
# {1..5}      <- one iteration, literal text

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

user=root
echo ~$user
# ~root       <- tilde expansion also runs before variables

go deeper

for a junior

Recall that a brace range cannot use a variable and that the loop silently runs once over the literal text. Know for ((i=1; i<=n; i++)) as the everyday replacement.

for a middle

Explain the cause in terms of stage order: brace expansion precedes parameter expansion, an invalid range is left literal, and no stage re-runs over an earlier stage's output.

for a senior

Show that you spot the failure mode in review — it errors nowhere, so a loop that should run n times runs once with a brace-laden value. Be ready to derive the tilde case from the same rule.

for a principal

Own the standard the team writes to: prefer constructs whose meaning does not depend on remembering the expansion pipeline, and treat any eval introduced only to win a second pass as a design smell to justify.

## What actually happens Bash expands a command line in fixed stages, and brace expansion is the very first one. When the shell reaches this line: ```bash n=5 for i in {1..$n}; do echo "$i"; done ``` the text it hands to brace expansion is literally `{1..$n}`. A sequence expression is only valid when both endpoints are integer literals (or single characters). `$n` is neither — at this instant it is three characters, `$`, `n`, and nothing has looked at variables yet. So the expansion does not apply, and bash follows its rule for an invalid brace construct: leave it exactly as written, no error, no warning. The next stage, parameter expansion, then does its job and replaces `$n` with `5`. The word is now `{1..5}` — but that is *output* of a completed stage, and bash does not go back and redo an earlier stage on it. Brace expansion has already happened for this command line and will not happen again. The `for` therefore has exactly one word in its list, and prints: ``` {1..5} ``` One iteration, with `i` set to the literal five-character string `{1..5}`. Nothing errors, which is what makes this bug survive code review — a loop that should have run five times ran once, and the body probably did something silly with a filename containing braces. ## The general rule this teaches Anything that happens *before* parameter expansion cannot see a variable's value. Brace expansion is the headline case, but the same order explains tilde expansion: ```bash user=alice echo ~$user # prints ~alice, not /home/alice ``` Tilde expansion also runs before parameter expansion. It looks at the characters after the tilde, sees the literal `$user`, finds no such login name, and leaves the tilde in place; the variable is expanded afterwards. The correct forms are `~alice` written literally, or `eval echo ~"$user"`, or simply reading the home directory from a source that is itself a parameter expansion, such as `$HOME` for the current user. The symmetric point is also worth internalising: anything that happens *after* parameter expansion does apply to the substituted text. Command substitution's output, and a variable's value, are still subject to the later stages of the same pass. So the direction of the asymmetry is precise — later stages see earlier results, earlier stages never see later ones. ## The fixes, in preference order **C-style loop.** The arithmetic `for` evaluates its expressions when the loop runs, long after any expansion decision, so the variable is simply read as a number: ```bash for ((i = 1; i <= n; i++)); do echo "$i" done ``` Note that inside `(( ))` you write `n`, not `$n` — the arithmetic context reads the variable itself. This is a bashism, not POSIX, but it forks nothing and handles an `n` of zero correctly by running the body zero times. **Command substitution over `seq`.** Command substitution happens after brace expansion, so the endpoints are already resolved: ```bash for i in $(seq 1 "$n"); do echo "$i"; done ``` This relies on the unquoted result being split into words, and it forks an external process. `seq` is not POSIX, though it is present on both GNU and BSD systems. **`eval`, deliberately.** `eval "for i in {1..$n}; do echo \"\$i\"; done"` does work: the string is expanded once by the outer command, producing `for i in {1..5}`, and then re-parsed from scratch, so brace expansion gets a second chance with a literal endpoint. It is the honest answer to "how do I force a second pass", but it is the wrong tool here because a plain arithmetic loop does the same job with no re-parsing at all — and putting anything you did not compute yourself inside `eval` is how shell injection happens. **A pre-built array.** If the values come from data rather than a range, collect them into an array and iterate `"${arr[@]}"`; no expansion-order trick is needed at all. ## What interviewers are checking The question is a proxy for whether you have internalised the expansion pipeline or merely memorised syntax. A candidate who says "braces don't work with variables" has memorised the symptom; a candidate who says "brace expansion is the first stage, so it cannot see a value that parameter expansion has not produced yet, and results are not re-scanned" can derive the tilde case, the `{1..n}` case, and the reason `eval` is the only way to get a second pass — all from one rule.

  • Why does `echo ~$USER` print something like `~alice` rather than the home directory?
    Same ordering cause. Tilde expansion runs before parameter expansion, so it inspects the literal characters `$USER`, finds no login name by that name, and leaves the tilde untouched; the variable is substituted afterwards. Tilde expansion does not get a second look at the result. Use `$HOME` for the current user, or a literal `~alice`.
  • If the loop had been written `for i in {1..n}`, what would it print?
    The single word `{1..n}`. There is no variable reference at all, so nothing changes it later; `n` is not an integer literal, the sequence expression is invalid, and bash leaves the braces as literal characters. It is the same silent one-iteration bug, and it is even easier to miss in review because it looks like a valid range.
  • Does `eval` fix this, and should you use it here?
    It does fix it — `eval` expands the string once and then parses the result from scratch, so brace expansion runs again on `{1..5}`. But it is the wrong tool for a counted loop: `for ((i=1; i<=n; i++))` gets the same result with no re-parse. Save `eval` for cases where the command structure itself is genuinely computed.

saying these in an interview costs you the question

  • Says the loop prints 1 through 5
  • Claims bash re-expands the result of a previous stage
  • Thinks quoting $n differently would fix it
  • Says it raises a syntax error rather than looping once
  • Reaches for eval before the arithmetic for loop

context