166

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

1
  • 1
    I tried running echo $x and it did print 1. Why did you expect it to print nothing?
    – palapapa
    Commented Mar 19 at 5:33

3 Answers 3

218

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.

14
  • 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. Commented Mar 3, 2018 at 22:08
  • 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}. Commented Mar 4, 2018 at 0:07
  • 2
    @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
    Commented Mar 12, 2018 at 11:13
  • 8
    @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 unix.stackexchange.com/q/261638 and unix.stackexchange.com/a/157962 Commented Mar 12, 2018 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
    Commented Mar 12, 2018 at 19:54
21

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, [...]

1
  • 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
    Commented Jun 21, 2014 at 23:50
1

Subshell Cost

Another way to consider this question is to look at the cost of running commands in parentheses. If the commands are running in a subshell, there ought to be some cost in processor cycles and time. E.g., two functions that are identical except for braces vs parentheses to encapsulate a list of commands:

foo() { 
    for i in $(seq $1)
    do 
        { 
            let 'j = 2*i - i'
            echo "run $j (subshell: $BASH_SUBSHELL)"
        }
    done
}

bar() { 
    for i in $(seq $1)
    do 
        ( 
            let 'j = 2*i - i'
            echo "run $j (subshell: $BASH_SUBSHELL)"
        )
    done
}

time foo 1000
# real    0m0.013s

time bar 1000
# real    0m0.596s

So the cost of spawning each subshell is ~ 0.6 msec.

I have found the cost is similar whether running simple or more complex commands in the subshell. This was run in Bash 5.2.21.

You must log in to answer this question.

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