1

When you have a function f defined in a Bash script, you can call it using $(f). However, this creates a subshell in which the function is called.

Consider this example:

#!/usr/bin/bash
f() {
    echo hello $X
}
X=world
echo f returns $(f)

The above script prints f returns hello world. What I don't understand is that the subshell inherits the current environment, including the local variable shell variable X. Local variables Shell variables aren't normally inherited in a subshell. So how does this work? And what is the actual command line passed to the bash subshell so as to call a function within a script?

(Edit: correct my terminology - I mean shell variable, not local variable. Kept original so that one of the answers makes sense.)

2
  • Are you confusing "subshell" with "child process"? I assume this is partially related to your question from last year: unix.stackexchange.com/questions/799429/… Commented Jan 22 at 10:21
  • @Kusalananda I think I must be confusing something. Doesn't a subshell run under a child process? In fact, I can see that when I execute the last line of the above script, the function f does indeed run within a child process which is also running /usr/bin/bash like its parent process. What is the distinction you are referring to? Commented Jan 22 at 10:46

2 Answers 2

2

Your $X isn't a local variable. Shell variables are global by default, which is why your $X is available to your function. You can define a local one using the local builtin, but that only makes sense (and only works) inside a function. For example:

#!/bin/bash
f() {
  local X="bar"
  echo "$X"
}
X="foo"
echo "f returns $(f), but X is $X"

Running the script above returns:

$ foo.sh
f returns bar, but X is foo

Even within a function, simply setting a variable doesn't make it local in bash:

#!/bin/bash
f() {
  X="bar"
}

echo "X was $X"
f
echo "And is now $X"

Running that gives:

$ foo.sh
X was
And is now bar

As you can see, the variable declaration inside the function changed the variable.

Finally, even if your variable were local, that wouldn't stop it from being inherited in a subshell if that subshell were running in the same scope as the variable:

#!/bin/bash
f() {
  local X="bar"
  echo "Outer function $BASHPID == $$"
  (
    echo "In the function's subshell ($BASHPID != $$), X is $X"
  )

}
f
echo "And is now '$X' because it is local to the function"

Which returns (see here for what the $BASHPID thing shows):

$ foo.sh
Outer function 800813 == 800813
In the function's subshell (800814 != 800813), X is bar
And is now '' because it is local to the function

A clarification on subshells vs child processes. While subshells are child processes, not all child processes are subshells. Variables don't need to be exported in order to be available in subshells, which is why you see the behavior you are seeing. They do need to be exported in order to be inherited by other kinds of child processes. For example, if I start a new bash instance, that is a child process, but it isn't a subshell, and so does not inherit unexported variables, and only gets variables if they have been exported:

$ echo $$
860946
$ var="foo"

$ bash
## We are now in a new bash instance, a child process with
## different PID
$ echo $$
861361

## This isn't a subshell
$ echo "$BASHPID == $$"
861361 == 861361

## the unexported variable isn't available
$ echo "$var"

$ exit
## We are back in the original bash instance
$ export var
## We now start a new one again
$ bash
## now that we exported it, it can be seen
$ echo "$var"
foo

So, subshells inherit variables. See section 3.2.3 in the GNU bash manual:

Each command in a multi-command pipeline, where pipes are created, is executed in its own subshell, which is a separate process (see Command Execution Environment).

Clicking on the "Command Execution Environment" takes you to section 3.7.3 which explains:

Shell parameters that are set by variable assignment or with set or inherited from the shell’s parent in the environment.

[ ... ]

A subshell is a copy of the shell process.

And also mentions (emphasis mine):

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. Unless otherwise noted, the values are inherited from the shell.

  • The shell’s open files, plus any modifications and additions specified by redirections to the command.
  • The current working directory.
  • The file creation mode mask.
  • Shell variables and functions marked for export, along with variables exported for the command, passed in the environment (see Environment).
  • Traps caught by the shell are reset to the values inherited from the shell’s parent, and traps ignored by the shell are ignored.

A command invoked in this separate environment cannot affect the shell’s execution environment.

This all boils down to: you don't need to export variables for them to be available in a subshell because subshells get an exact copy of their parent execution environment. You do need to export if you want them available in other child processes. Such as a bash -c invocation.

0
1

Subshell runs as bash child after fork and inherits the complete environment of its parent including all variables and functions. Ask yourself where subshell gets the definition of f from.

3
  • Thanks. If I do this in a terminal: f() { echo Hi!; } bash -c f I get f: command not found so the subshell doesn't inherit functions from the parent. Commented Jan 22 at 11:49
  • 1
    Subshell first executes new /bin/bash which throws away the inherited parent environment. Commented Jan 22 at 11:55
  • 1
    @k314159 that's because a new invocation of bash is not a subshell. Commented Jan 22 at 12:30

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.