4

Why does

for i in {1..5}; do x="${i}" echo "$x"; done

not output

1
2
3
4
5

?

What is the right way to do this?

(Tested for i in {1..5}; do x=$(i) echo "$x"; done

-bash: i: command not found

and others)

3
  • 2
    $(i) (command substitution) is not the same thing as ${i} (variable expansion) Nov 20 '17 at 0:51
  • @steeldriver Thank you very much for this hint! Now one can understand the error message.
    – user122618
    Nov 20 '17 at 1:07
  • Thank you all for your great answers! The problem was that I forgot a semicolon.
    – user122618
    Nov 20 '17 at 17:25
10

Answering your question as asked

Why does for i in {1..5}; do x="${i}" echo "$x"; done not output 1, 2, 3, 4, 5?

The reason is to do with the order of which operations are evaluated during execution. Look at this command

x="${i}" echo "$x"

What this does is

  1. Substitute variables with their values
  2. Assign a temporary value to variable x
  3. Execute the command

So you get

  1. x=1 echo "" (or x=2 echo "", etc.)
  2. For the duration of this command, x is set to the value 1
  3. The command is executed: echo ""

It's probable you meant this command to be two instructions: assign a value to x, output its value. But in shell syntax what you wrote was perfectly legal code so the shell executed it without objection.

9
  • +1 The better answer, because it explains what happens without the semicolon!
    – Philippos
    Nov 20 '17 at 8:10
  • @roaima: Your explanation is not entirely correct. The crucial point in step 2 is that the variable x is placed in the execution environment (i.e. child process) for the command to be executed and hence not visible to the parent process. This can be illustrated by changing the example to for i in {1..5}; do x="${i}" printenv x; done, which outputs the expected 1,2,...,5. Nov 20 '17 at 10:25
  • @user1934428 that's a side-effect of what I have described. Try this as a counter example - x=apple; for i in {1..5}; do x="${i}" echo "$x"; done
    – roaima
    Nov 20 '17 at 11:18
  • @Philippos It's possible that I may have misunderstood the source of confusion in the OP. I assumed that they were confused about the for-loop syntax rather than the command itself. I assumed that if the question were really about the command itself then the OP wouldn't have included the for-loop, since it would have been superfluous. But I was inspired by your comment to update my solution.
    – igal
    Nov 20 '17 at 13:09
  • @user1934428 I think that roaima's explanation is correct. I updated my solution to included references to the manual section that explicitly states how variable assignments in commands are evaluated.
    – igal
    Nov 20 '17 at 13:11
6

You asked for answers to two questions:

  1. You asked for an explanation of why your current code does not produce the expected output.

  2. You asked for the correct way to write your code so that it does produce the expected output.

Looking at your code, I can see two likely explanations for why you wrote your code the way you did:

  1. There might be some slight confusion about the syntax of a for-loop.

  2. There might be some slight confusion about the order of evaluation in what's called a simple command.

for-loop syntax

In the first case, I would say that you're missing a semicolon after your variable assignment. If you want to write your for-loop on a single line then you need to put a semi-colon after each command in the loop body. Try this instead:

for i in {1..5}; do x="${i}"; echo "$x"; done

Another alternative would be to write the for-loop using the multi-line syntax with newlines in place of the semicolons:

for i in {1..5}
do
x="${i}"
echo "${x}"
done

You can also mix-and-match semicolons and newlines, e.g.:

for i in {1..5}; do
x="${i}"; echo "${x}"
done

evaluation of simple commands

In the second case, I would say that you probably had assumed that the variable assignment in the prologue of the command (i.e. the x="$i" assignment) occurs before the variable expansion in the body of the command (i.e. the expansion of ${x} in echo "${x}"). But this is actually not the case. To verify this we can refer to the page on simple command expansion in the Bash Manual or to the subsection on simple commands in the Posix Specification. Both of these references include the following passage:

A "simple command" is a sequence of optional variable assignments and redirections, in any sequence, optionally followed by words and redirections, terminated by a control operator.

When a given simple command is required to be executed (that is, when any conditional construct such as an AND-OR list or a case statement has not bypassed the simple command), the following expansions, assignments, and redirections shall all be performed from the beginning of the command text to the end:

  1. The words that are recognized as variable assignments or redirections according to Shell Grammar Rules are saved for processing in steps 3 and 4.

  2. The words that are not variable assignments or redirections shall be expanded. If any fields remain following their expansion, the first field shall be considered the command name and remaining fields are the arguments for the command.

  3. Redirections shall be performed as described in Redirection.

  4. Each variable assignment shall be expanded for tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal prior to assigning the value.

Notice that step 2 is where the variable expansion in the command occurs, but step 1 tells us that the variable assignments are saved until steps 3 and 4. It follows that the expression echo "${x}" is expanded to echo "" before the assignment x="${i}" takes place. This explains why you were getting empty output.

For further discussion on this topic see the following posts:

14
  • 1
    All that verbiage, when the answer is "you forget a semicolon"...
    – RonJohn
    Nov 20 '17 at 15:25
  • @RonJohn That's what I had originally thought too, but apparently more people thought that the real issue was related to the command parsing. See the comments after the solution posted by roaima.
    – igal
    Nov 20 '17 at 15:30
  • @RonJohn Given the number of people who think that precommand environment modifiers should apply to echo, "you forgot a semicolon" isn't necessarily the most helpful answer.
    – chepner
    Nov 20 '17 at 19:59
  • @igal semicolons are part of the language, and required to correctly parse when "everything on a single line". Thus, the forgotten semicolon made it parse incorrectly (if by "incorrectly" you mean "didn't print 1 2 3 4 5".
    – RonJohn
    Nov 20 '17 at 20:13
  • @igal also, I made the same mistake earlier this morning... :(
    – RonJohn
    Nov 20 '17 at 20:14
1

In zsh, using a third ${var::=value} form of the ${var=value}, ${var:=value} Bourne operators where the assignment is unconditional (as opposed to only if var is unset/empty).

for i in {1..5}; do echo ${x::=$i}; done

Or:

for i ({1..5}) echo ${x::=$i}

In bash:

set -o posix # so the value of x remains after eval returns
for i in {1..5}; do x=$i eval 'echo "$x"'; done

That is, you need $x to have been set at the time the code that contains its expansion is evaluated.

For decimal integer values like here, you can also do:

for i in {1..5}; do echo "$((x = i))"; done

Or you can always use the ${var:=value} Bourne operator after having set x to the empty string.

for i in {1..5}; do x=; echo "${x:=$i}"; done

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy