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