BASH Subshell Gotchas

It's always helpful to remember that not all BASH operations are created equal. In dicing with configuring an environment I failed to recall the first rule of BASH scripting: Know Your Subshells!

In this exampe n is always 0. This is because the increment operation is in a subshell, and variables do not propagate back to their parent shells.

n=0
cat file | while read line; do (( n++ )); done
echo $n

Environment variables, as well as the current directory, is only inherited parent-to-child. Changes to a child’s environment are not reflect in the parent. Any time a shell forks, changes done in the forked process are confined to that process and its children.

From the Advanced Bash-Scripting Guide on Subshells:

In general, an external command in a script forks off a subprocess, whereas a Bash builtin does not. For this reason, builtins execute more quickly and use fewer system resources than their external command equivalents.

So, where does BASH create a subshell?

  • executing a program or script
  • for every invocation in a pipeline
  • any time a new shell is started
  • for background commands and coprocesses
  • in-shell command expansion
  • in-shell process substitution
  • explicit subshells

So to prevent a subshell being forked, use exec or a builtin.

To wit:

# Executing other programs or scripts
./setmyfoo
foo=bar ./something

# Anywhere in a pipeline in Bash
true | foo=bar | true

# In any command that executes new shells
awk '{ system("foo=bar") }'h
find . -exec bash -c 'foo=bar' \;

# In backgrounded commands and coprocs:
foo=bar &
coproc foo=bar

# In command expansion
true "$(foo=bar)"

# In process substitution
true < <(foo=bar)

# In commands explicitly subshelled with ()
( foo=bar )

Source: Vidar’s Blog » Why Bash is like that: Subshells - contains further examples