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.
fdoes indeed run within a child process which is also running/usr/bin/bashlike its parent process. What is the distinction you are referring to?