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)
Answering your question as asked
Why does
for i in {1..5}; do x="${i}" echo "$x"; done
not output1
,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
x
So you get
x=1 echo ""
(or x=2 echo ""
, etc.)x
is set to the value 1
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.
for i in {1..5}; do x="${i}" printenv x; done
, which outputs the expected 1,2,...,5.
– user1934428
Nov 20 '17 at 10:25
x=apple; for i in {1..5}; do x="${i}" echo "$x"; done
– Chris Davies
Nov 20 '17 at 11:18
echo $x
by printenv x
, we get 1,2,3.... again. The x=... in front of a command does set the execution environment for the subsequent command, but the "$x" is, of course, still evaluated in the current environment and hence does not "see" the change of x.
– user1934428
Nov 20 '17 at 16:30
printenv
suggestion doesn't try to evaluate the value of a variable that's being set for the duration of the command. In that respect it's irrelevant, and the (valid) effect of copying that variable to the environment is a complete side-issue. In the context of the question, and the way that I answered it, to which particular part do you object?
– Chris Davies
Nov 20 '17 at 19:45
X=U V
, where V is an external program, spawns a child process, where V is started. This child process inherits the environment of the parent (i.e. the current shell, with the difference that the variable X is set to the value U. This means X becomes an environment variable in the child (and only then). If I have instead X=U; V
, the variable X is set in the current process, and doesn't necessarily show up in the environment of V.
– user1934428
Nov 21 '17 at 06:07
X=1 foo $X
, foo does not get passed the value 1 as argument (but sees the environment variable X to be set to 1). We can see this nicely in this example: export A=5; export B=6; C=A; C=B printenv $C
where 5 is printed, because $C evaluates to A and not to B. From your explanation, I had the impression that you wanted to say that in such an example, C=B would temporarily set C in the current shell, and I didn't find this accurate; but maybe I misunderstood you in this point?
– user1934428
Nov 21 '17 at 06:10
You asked for answers to two questions:
You asked for an explanation of why your current code does not produce the expected output.
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:
There might be some slight confusion about the syntax of a for-loop.
There might be some slight confusion about the order of evaluation in what's called a simple command.
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
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:
The words that are recognized as variable assignments or redirections according to Shell Grammar Rules are saved for processing in steps 3 and 4.
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.
Redirections shall be performed as described in Redirection.
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:
echo
, "you forgot a semicolon" isn't necessarily the most helpful answer.
– chepner
Nov 20 '17 at 19:59
1 2 3 4 5
".
– RonJohn
Nov 20 '17 at 20:13
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
$(i)
(command substitution) is not the same thing as${i}
(variable expansion) – steeldriver Nov 20 '17 at 00:51