164

From what I've read, putting a command in parentheses should run it in a subshell, similar to running a script. If this is true, how does it see the variable x if x isn't exported?

x=1

Running (echo $x) on the command line results in 1

Running echo $x in a script results in nothing, as expected

Kevin
  • 40,767
Igorio
  • 7,519
  • 7
  • 20
  • 11

2 Answers2

211

A subshell starts out as an almost identical copy of the original shell process. Under the hood, the shell calls the fork system call1, which creates a new process whose code and memory are copies2. When the subshell is created, there are very few differences between it and its parent. In particular, they have the same variables. Even the $$ special variable keeps the same value in subshells: it's the original shell's process ID. Similarly $PPID is the PID of the parent of the original shell.

A few shells change a few variables in the subshell. Bash ≥4.0 sets BASHPID to the PID of the shell process, which changes in subshells. Bash, zsh and mksh arrange for $RANDOM to yield different values in the parent and in the subshell. But apart from built-in special cases like these, all variables have the same value in the subshell as in the original shell, the same export status, the same read-only status, etc. All function definitions, alias definitions, shell options and other settings are inherited as well.

A subshell created by (…) has the same file descriptors as its creator. Some other means of creating subshells modify some file descriptors before executing user code; for example, the left-hand side of a pipe runs in a subshell3 with standard output connected to the pipe. The subshell also starts out with the same current directory, the same signal mask, etc. One of the few exceptions is that subshells do not inherit custom traps: ignored signals (trap '' SIGNAL) remain ignored in the subshell, but other traps (trap CODE SIGNAL) are reset to the default action4.

A subshell is thus different from executing a script. A script is a separate program. This separate program might coincidentally be also a script which is executed by the same interpreter as the parent, but this coincidence doesn't give the separate program any special visibility on internal data of the parent. Non-exported variables are internal data, so when the interpreter for the child shell script is executed, it doesn't see these variables. Exported variables, i.e. environment variables, are transmitted to executed programs.

Thus:

x=1
(echo $x)

prints 1 because the subshell is a replication of the shell that spawned it.

x=1
sh -c 'echo $x'

happens to run a shell as a child process of a shell, but the x on the second line has no more connection with the x on the second line than in

x=1
perl -le 'print $x'

or

x=1
python -c 'print x'

1 Unless the shell optimizes the forking out, but emulates forking as much as necessary to preserve the behavior of the code that it's executing. Ksh93 optimizes a lot, other shells mostly don't.
2 Semantically, they're copies. From an implementation perspective, there's a lot of sharing going on.
3 For the right-hand side, it depends on the shell.
4 If you test this out, note that things like $(trap) may report the traps of the original shell. Note also that many shells have bugs in corner cases involving traps. For example ninjalj notes that as of bash 4.3, bash -x -c 'trap "echo ERR at \$BASH_SUBSHELL \$BASHPID" ERR; set -E; false; echo one subshell; (false); echo two subshells; ( (false) )' runs the ERR trap from the nested subshell in the “two subshells” case, but not the ERR trap from the intermediate subshell — set -E option should propagate the ERR trap to all subshells but the intermediate subshell is optimized away and so isn't there to run its ERR trap.

  • Doesn't (echo $x) print the value of x because of the parent shell doing variable substitution on $x before forking the sub-shell? – Kusalananda Apr 09 '17 at 13:04
  • 2
    @Kusalananda No. (x=out; (x=in; echo $x)) – Gilles 'SO- stop being evil' Apr 09 '17 at 13:05
  • @Gilles Regarding your last comment to Kusalananda, the evidence shows that you are right. But why does the manual say "The order of expansions is: brace expansion; tilde expansion, parameter and variable expansion, arithmetic expansion, and command substitution (done in a left-to-right fashion); word splitting; and filename expansion." Isn't this kind of misleading? I had interpreted the manual to mean $x would be expanded first as it has higher precedence than command substitution. But apparently this is not the case, as you correctly point out. – flow2k Mar 03 '18 at 20:01
  • 3
    @flow2k This is the order of expansion for things happening at the same level. But you also need to consider how expansion is mixed with evaluation. When expansion requires the evaluation of a nested construct, the inner construct is evaluated first. So, for example, to evaluate echo $(x=2; echo $x), the fragment $(x=2; echo $x) needs to be expanded. This requires evaluating the command x=2; echo $x. The expansion of $x happens during this evaluation, after evaluating the part x=2. – Gilles 'SO- stop being evil' Mar 03 '18 at 22:08
  • @Gilles Thanks! With this in mind (order of expansion is for things at the same level), it would then appear that it is superfluous to assign an order between parameter expansion and command substitution - the manual does not add any information in indicating the order between the two. In other words, I cannot find an example where the results would be different had command substitution happened before parameter expansion (keeping in mind this order we're discussing is for things at the same level). – flow2k Mar 03 '18 at 23:42
  • 3
    @flow2k There is no order between parameter expansion and command substitution. Note that this sentence uses semicolons to separate expansion steps, but parameter expansion and command substitution are in the same semicolon-delimited clause (yes, it's subtle). The order matters when one of the parts has a side effect that affects the other part, e.g. (with x unset) echo $(echo foo >somefile)${x-$(cat somefile)} or echo $(echo $x),${x=1}. – Gilles 'SO- stop being evil' Mar 04 '18 at 00:07
  • @Gilles Ah hah! I was not careful in reading that sentence in the manual. Yes, parameter expansion and command substitution are equal in precedence and whichever comes first (more left) gets executed earlier. The last example (echo $(echo $x),${x=1}) is a good one - I was initially confused, but now I see your point. Thanks! – flow2k Mar 04 '18 at 00:55
  • 1
    @Gilles; I am confused. If a subshell is different than executing a script then why it is said that: Running a shell script launches a new process, a subshell.? Also, A subshell environment shall be created as a duplicate of the shell environment. Therefore, ./file will be executed in subshell environment and thus it should inherit shell parameters that are set by variable assignment. – haccks Mar 12 '18 at 11:13
  • 7
    @haccks The definition in the ABS is an approximation, and not a very good one. The examples are good, but the first two lines of that page are so oversimplified that they're wrong. Running a script from another script launches a new process which is not a subshell. In the SUS, the definitions are correct (but not always very easy to understand). ./file is not executed in a subshell. See also https://unix.stackexchange.com/q/261638 and https://unix.stackexchange.com/a/157962 – Gilles 'SO- stop being evil' Mar 12 '18 at 18:52
  • 2
    @Gilles; This is sad that official doc has this mistake! So, subshell is a child process which executes in the parent shell environment while external command execution is a child process but have different execution environment, am I right? – haccks Mar 12 '18 at 19:54
  • 1
    @haccks That's correct. – Gilles 'SO- stop being evil' Mar 12 '18 at 22:24
  • 1
    Material for a 4th footnote: bash's doc states that a subshell duplicates the environment of the parent except for traps, which are reset to the values inherited by the shell from its parent at invocation. Testing seems to confirm it. – ninjalj Apr 03 '18 at 15:43
  • 1
    It also seems that bash incorrectly optimizes trap 'echo ERR at $BASH_SUBSHELL' ERR; set -E; false; echo one subshell; (false); echo two subshells; ( (false) ): in the last command, subshell 1 should run the trap, but it doesn't, due to subshell 1 directly execing subshell 2 (workaround, add some command to subshell 1, as in ( : ; (false) ) – ninjalj Apr 03 '18 at 15:50
20

Obviously, yes, as all the documentation says, a parenthesized command is run in a subshell.

The subshell inherits a copy of all the parent's variables. The difference is that any changes you make in the subshell aren't also made in the parent.

The ksh man page makes this a little clearer than the bash one:

man ksh:

A parenthesized command is executed in a sub-shell without removing non-exported variables.

man bash:

(list)

list is executed in a subshell environment (see COMMAND EXECUTION ENVIRONMENT below). Variable assignments and builtin commands that affect the shell's environment do not remain in effect after the command completes.

COMMAND EXECUTION ENVIRONMENT

The shell has an execution environment, which consists of the following: [...] shell parameters that are set by variable assignment [...].
Command substitution, commands grouped with parentheses, and asynchronous commands are invoked in a subshell environment that is a duplicate of the shell environment, [...]

Toby Speight
  • 8,678
Mikel
  • 57,299
  • 15
  • 134
  • 153
  • 3
    This is to be contrasted to When a simple command other than a builtin or shell function is to be executed, it is invoked in a separate execution environment that consists of the following., which contains the item: · shell variables and functions marked for export, along with variables exported for the command, passed in the environment (from the same man bash section) which explains why an echo $x-script prints nothing if x is not exported. – Johan E Jun 21 '14 at 23:50