8

I have the following in a script:

#!/usr/bin/env bash 
...
for i in {1..$N}
   <loop content>

The above doesn't work and is only executed once where i has the value {1..5} (if 5 was provided as argument).

If I replace the above with:

for i in $(seq 1 1 $N)

.. then things work as expected. Why is that?

Marcus Junius Brutus
  • 4,587
  • 11
  • 44
  • 65
  • This is a duplicate of this Q: http://unix.stackexchange.com/questions/7738/how-can-i-use-var-in-a-shell-brace-expansion-of-a-sequence as well as this one: http://unix.stackexchange.com/questions/7998/which-bash-will-expand-1-var-in-the-same-way-that-zsh-does – slm Nov 29 '13 at 20:52
  • @1_CR - you shouldn't have deleted that comment. When you did it that removed the duplicate Q that I was using to close this Q as a dup. – slm Nov 29 '13 at 21:47
  • @slm, ooops, sorry. Faux pas on my part – iruvar Nov 30 '13 at 01:07

2 Answers2

11

{1..8} is a form of brace expansion introduced by zsh and supported by recent versions of bash and ksh93. In bash, it is done very early. In particular, it is done before variable expansion.

While {1..8} is valid, {1..$N} is not because $N is not a decimal number, it's the two characters $ and N none of which are decimal digits.

Even if that worked, that would not be very efficient. The shell would have to allocate in memory a list made of the numbers 1 to $N. If $N was 109, that would mean allocating gigabytes of memory.

$(seq "$N") is even less efficient. First, it's not portable. seq is a non-standard GNU command. Then doing $(seq "$N") means starting a new process, executing seq in it, the shell reading its output via a pipe and storing it in memory, then splitting that list (according to $IFS) and attempt to perform filename generation on it.

The space for the list resulting of that splitting and globbing has to be allocated as well.

A more efficient way to do it is to use the ksh for (( )) style of loop (also supported by bash and zsh for decades).

for ((i = 1; i <= N; i++)); do
   <loop content>
done

Or simply write it the portable and standard way, so you don't even have to use bash, and can use your (probably faster/leaner) system's sh:

i=1
while [ "$i" -le "$N" ]; do
  <loop content>
  i=$(($i + 1))
done

Or maybe you can do it more the shell way by avoiding loops altogether. But we'd need to know what you want to do eventually to help there.

2

@1_CR is right. This is a duplicate. The brace expansions happen first, before the variable, $N, has been expanded. Hence why the first example doesn't work. You're trying to brace expand 1 to a literal $N.

Example

What you're trying to do:

$ N=5
$ echo {1..$N}
{1..5}

When real values are used:

$ echo {1..5}
1 2 3 4 5

Also you're not really comparing apples to apples here. What gives you the impression that {1..$N} is equiv. to $(seq 1 1 $N)?

Other than producing a sequence of numbers, the braces ({..}) and the sub-shell ($(..)`) are 2 completely different notations.

slm
  • 369,824
  • You can use eval echo ${1..$N}. That will expand it like expected. But be careful with eval, as eval is evil and if it's possible to avoid its use, it's better to do it. – rush Nov 29 '13 at 20:48
  • As a general rule of thumb I never use eval. It's a foolish command that shouldn't be included in the language (IMO)! – slm Nov 29 '13 at 20:50
  • eval has valid uses. Rare, – phemmer Nov 29 '13 at 21:49
  • @Patrick - sure, but there are more cases where you'll get burned, so better to avoid it. Generally if you need to use it, then your doing it wrong, IMO. – slm Nov 29 '13 at 21:54