3

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?

2
  • 2
    Does it help to think about the precedence of the operators ; and |? So, your first command sequence a=1; a=2; a=10 | echo $a is equivalent to a=1; a=2; (a=10 | echo $a).
    – celadon
    Mar 11, 2020 at 3:26
  • 1
    @celadon, though be careful with notation like that, since in the shell language, the parenthesis invoke a subshell which otherwise might not be there. (it doesn't change the behaviour here, but might matter in some other pipeline with lastpipe set etc.)
    – ilkkachu
    Mar 11, 2020 at 20:58

3 Answers 3

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?

8
  • 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. I Mar 12, 2020 at 4:35
  • @FuRinKaZan_001 They are executed in no order we can rely on. See, for details, this Q/A on this site.
    – fra-san
    Mar 12, 2020 at 8:52
  • @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
    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
    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 and a=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 elementary a=1 list is ok as a pipeline command, while a=1; a=2; or even a=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
    Mar 12, 2020 at 14:51
1

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 the stdout a command to stdin of command2, i.e command | command2.

  • a=10 is a variable assignment, I'll assume there is not stdout 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 value 10. When I modified it to

    user@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 value 1 initially, then the variable's value is changed to 2, 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 as echo doesn't take stdin, but only prints it's argument. Here, the argument is $a, the value of variable a in the sub-shell which is 2.

  • Furthermore, a=10 doesn't produce any output as it's a variable assignment. So, in effect echo $a is printing its argument's value which is 2 and not taking anything from a=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 produces 10.

  • 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 of a is 10 in the sub-shell in which the command echo is also executed, so it returns 10. After you get your prompt, if you issue the command echo $a, it returns 2 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.

3
  • Amazing text. I think the key point to understand is:
    – JohnC
    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
    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
    Mar 12, 2020 at 14:45
1

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.)

This site is temporarily in read-only mode and not accepting new answers.

Not the answer you're looking for? Browse other questions tagged .