Ok, I know that in Bash (by default, without 'lastpipe' bash option enabled) every variable assigned after a pipeline is, indeed, executed in a subshell and the variable itself dies after subshell execution. It does not remain available to the parent process. But doing some tests, I came up with this behavior:
A) Second command (a=2) assign the value and is returned:
[root@centos01]# a=1; a=2; a=10 | echo $a
2
B) Third command (a=10) assign the value and is returned:
[root@centos01]# a=1; a=2; a=10; a=20 | echo $a
10
C) Fourth command (a=20) assign the value and is returned:
[root@centos01]# a=1; a=2; a=10; a=20; touch fileA.txt | echo $a
20
So:
Why always the last variable assignment in command's sequence is not actually executed? (or if it is, why it is not being caught by the subshell and returned back by echo command?)
In test C, the 'touch' command actually created the file 'fileA.txt' in the directory. So, why the last command in the sequence for variable assignment made in step A and set B did not work? Does anyone know the technical explanation for that?
3 Answers 3
First, to agree on a few names, here is how the shell is interpreting your input:
$ a=1; a=2; a=10 | echo $a
^^^ ^^^ ^^^^^^^^^^^^^^
\ \ \_ Pipeline
\ \_ Simple command
\_ Simple command
The pipeline is composed of two simple commands:
$ a=10 | echo $a
^^^^ ^^^^^^^
\ \_ Simple command
\_ Simple command
(Note that, while it may not be clearly stated in Bash's manual, the POSIX shell grammar allows a simple command to be constituted of a mere variable assignment).
a=1;
and a=2;
are not part of any pipeline. A ;
would terminate a pipeline, except when appearing as part of a compound command. As in, for instance:
{ a=1; a=2; a=10; } | echo $a
In your example, a=10
and echo $a
are executed in two distinct, independent subshell environments1, both created as copies of the main environment. Subshells are required not to alter their parent execution environment2. Quoting the relevant POSIX section:
A subshell environment shall be created as a duplicate of the shell environment [...] Changes made to the subshell environment shall not affect the shell environment.
and
Additionally, each command of a multi-command pipeline is in a subshell environment; as an extension, however, any or all commands in a pipeline may be executed in the current environment. All other commands shall be executed in the current shell environment.
Thus, while all the commands in your examples are actually executed, the assignments in the left hand part of your pipelines have no visible effect: they only alter the copies of a
in their respective subshell environments, which are lost as soon as the subshells terminate.
The only way the subshells at the two ends of a pipe can directly interact with each other is by means of the pipe itself—the standard output of the left hand side connected to the standard input of the right hand side. Since a=10
doesn't send anything over the pipe, there is no way it can affect echo $a
.
1 If the lastpipe
option is set (it is off by default and can be enabled using the shopt
builtin), Bash may execute the last command of a pipeline in the current shell. See Pipelines in Bash manual. However, this is not relevant in the context of your question.
2 You may find more details from a practical/historical perspective on U&L, e.g. in this answer to Why doesn't the last function executed in a POSIX shell script pipeline retain variable values?
-
Hello, what is the order of commands executed in a pipeline? I tried executing the poster's test case with bash debug option(set -x) enabled. The echo command in the pipeline was executed before the variable assignment was set. IFuRinKaZan_001– FuRinKaZan_0012020年03月12日 04:35:42 +00:00Commented Mar 12, 2020 at 4:35
-
-
@fra-san Excelent explanation. But is it correct to say that a=10 and echo $a are being executed in two distinct subshells? My understanding after reading everything was: the pipeline created a "list", The TWO commands are executed in the same subshell. The reason the echo did not show the value 10 (even with the variable assignment actually being done) is because echo command got executed first, before a=10 got in place. New "echos" don't produce 10 as value because a=10 died when the subshell finished. Is that the correct concept?JohnC– JohnC2020年03月12日 14:01:04 +00:00Commented Mar 12, 2020 at 14:01
-
@JohnC Yes, two distinct subshells, Bash's manual states it clearly: "A pipeline is a sequence of one or more commands separated by one of the control operators ‘
|
’ or ‘|&
’" and "Each command in a pipeline is executed in its own subshell, which is a separate process". This looks even clearer than the POSIX spec I quoted.fra-san– fra-san2020年03月12日 14:37:44 +00:00Commented Mar 12, 2020 at 14:37 -
@JohnC IMO, "list" isn't really helpful there. All of
a=1
,a=1; a=2; a=10 | echo $a
anda=10 | echo $a
actually match the definition of "list". OTOH, pipelines are composed of commands (see my previous link) and a list may or may not match the definition of "command". E.g. the elementarya=1
list is ok as a pipeline command, whilea=1; a=2;
or evena=2;
(note the semicolon) aren't. To match the definition of "command" in the context of a pipeline, they'd need to be written as{ a=1; a=2; }
and{ a=2; }
.fra-san– fra-san2020年03月12日 14:51:08 +00:00Commented Mar 12, 2020 at 14:51
Please excuse my English, I'm still learning it. I am also a Bash beginner learner, so please correct any mistakes I've made in my answer, thank you.
First, I'll point out an error.
In
a=10 | echo $a
you're piping (using the pipe operator;|
)a=10
to echo command. Piping will connect thestdout
a command tostdin
of command2, i.ecommand | command2
.a=10
is a variable assignment, I'll assume there is notstdout
for it, as it's not a command. If you perform a command substitution in the value of a variable assignment, it doesn't work. If I try the following:user@host$ a=$(b=10); echo $a
the
echo $a
doesn't return the value10
. When I modified it touser@host$ a=$(b=10; echo $b)
a call of
$ echo $a
returned
10
. So, I'm probably right in assuming variable assignment is not a command (even by bash manual definition it isn't a command).
Secondly, the echo
command doesn't take input from stdin
, it prints its arguments.
user@host$ echo "I love linux" | echo
will return nothing. You can use xargs
command to overcome this:
user@host$ echo "I love linux" | xargs echo
will return I love linux
. So, piping doesn't work directly on echo
command as it prints its arguments and not stdin
.
Now, to your tests
In your command
user@host$ a=1; a=2; a=10 | echo $a
the variable
a
is assigned the value1
initially, then the variable's value is changed to2
, both in the current shell environment. Commands are generally run in sub-shell.a=10 | echo $a
is a list, i.e equivalent to(a=10 | echo $a)
which is run in a sub-shell, but it doesn't work asecho
doesn't takestdin
, but only prints it's argument. Here, the argument is$a
, the value of variablea
in the sub-shell which is2
.Furthermore,
a=10
doesn't produce any output as it's a variable assignment. So, in effectecho $a
is printing its argument's value which is2
and not taking anything froma=10 | < ... >
. So, the pipe operator should not be used here; instead, you can separate the command name and variable assignment with a terminator (, semi-colon), and it'll work fine, as in(a=10; echo $a)
.
To understand this better, you can try these following examples with bash debug option enabled:
user@host$ a=1; a=2; a=10; echo $a;
In the above command line,
echo $a
produces10
.If I change it to
user@host$ a=1; a=2; (a=10; echo $a)
the first and second variable assignment is set in the current shell, the third variable assignment and
echo
command is executed in a sub-shell. Therefore, the value ofa
is10
in the sub-shell in which the commandecho
is also executed, so it returns10
. After you get your prompt, if you issue the commandecho $a
, it returns2
as the variable assignments from the sub-shell don't carry back to the parent shell.- One more thing to note, the
;
command separator executes commands sequentially.
In test cases "A" and "B", the last variable assignment (a=10
in test A, and a=20
in test B) is actually executed, but it's executed after the echo $a
command, so you get the result of the previous value of variable a
which is in the sub-shell environment, after which the last variable assignments are executed. In a pipeline the stdin
and stdout
of two commands are connected before the command is executed, also variable assignments don't produce anything on stdout.
tl;dr: Variable assignment should not be be used in a pipeline. echo
doesn't work directly in a pipeline.
-
Amazing text. I think the key point to understand is:JohnC– JohnC2020年03月12日 14:36:30 +00:00Commented Mar 12, 2020 at 14:36
-
Amazing text. I think the key point to understand is: 1) a=10 and echo $a are both executed together in a distinct subshell. 2) The connection of left stdout to right stdin is made first of execution of command at left side of pipe. So, 'echo $a' outputs value from $a that the current subshell got from parent shell. No stdout received, so, it just prints the variable itself. After that the variable assignment is made and variable 'a' receives 10. All of that dies after subshell execution and value a=2 remains in the parent. Is all correct?JohnC– JohnC2020年03月12日 14:42:49 +00:00Commented Mar 12, 2020 at 14:42
-
I guess the confusion was made because I was expecting that a=10 got first then echo $a, and it should print in the subshell the value of 10. The a=10 does really occur, but only after echo command itself. That is why I always keep seeing the value of 2 instead of 10.JohnC– JohnC2020年03月12日 14:45:24 +00:00Commented Mar 12, 2020 at 14:45
Here,
a=20 | echo $a
the pipe just adds confusion. The assignment on the left doesn't print anything to stdout, and echo
doesn't read anything from stdin so no data is moved through the pipe. The argument to echo
is just expanded from what was set previously.
As you said, the parts of a pipeline run in distinct subshells, so the assignment to a
on the left hand side doesn't influence the expansion on the right hand side, and neither would such communication happen in the reverse case.
Instead, if you did this:
{ a=999; echo a=$a; } | cat
the pipe would make more sense, and the string a=999
would be passed through it.
The touch fileA.txt
in the last example works because it affects the system outside the shell. Similarly you could write to stderr from the commands in the pipe and see the result appear on the terminal:
$ echo a >&2 | echo b >&2 | echo c >&2
b
c
a
(That's indeed the order of output I got with Bash on my system.)
;
and|
? So, your first command sequencea=1; a=2; a=10 | echo $a
is equivalent toa=1; a=2; (a=10 | echo $a)
.lastpipe
set etc.)