Bash can seem pretty random and weird at times, but most of what people see as quirks have very logical (if not very good) explanations behind them. This series of posts looks at some of them.
# I run this script, but afterwards my PATH and current dir hasn't changed! #!/bin/bash export PATH=$PATH:/opt/local/bin cd /opt/games/
or more interestingly
# Why does this always say 0? n=0 cat file | while read line; do (( n++ )); done echo $n
In the first case, you can add a echo "Path is now $PATH"
, and see the expected path. In the latter case, you can put a echo $n
in the loop, and it will count up as you’d expect, but at the end you’ll still be left with 0.
To make things even more interesting, here are the effects of running these two examples (or equivalents) in different shells:
set in script | set in pipeline | |
Bash | No effect | No effect |
Ksh/Zsh | No effect | Works |
cmd.exe | Works | No effect |
What we’re experiencing are subshells, and different shells have different policies on what runs in subshells.
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.
In Unix, all normal shells will fork to execute other shell scripts, so setting PATH or cd’ing in a script will never have an effect after the command is done (instead, use "source file"
aka ". file"
to read and execute the commands without forking).
However, shells can differ in when subshells are invoked. In Bash, all elements in a pipeline will run in a subshell. In Ksh and Zsh, all except the last will run in a subshell. POSIX leaves it undefined.
This means that echo "2 + 3" | bc | read sum
will work in Ksh and Zsh, but fail to set the variable sum
in Bash.
To work around this in Bash, you can usually use redirection and process substition instead:
read sum < <(echo "2 + 3" | bc)
So, where do we find subshells? Here are a list of commands that in some way fails to set foo=bar for subsequent commands (note that all the examples set it in some subshell, and can use it until the subshell ends):
# 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 )
and probably some more that I'm forgetting.
Trying to set a variable, option or working dir in any of these contexts will result in the changes not being visible for following commands.
Knowing this, we can use it to our advantage:
# cd to each dir and run make for dir in */; do ( cd "$dir" && make ); done # Compare to the more fragile for dir in */; do cd "$dir"; make; cd ..; done # mess with important variables fields=(a b c); ( IFS=':'; echo ${fields[*]}) # Compare to the cumbersome fields=(a b c); oldIFS=$IFS; IFS=':'; echo ${fields[*]}; IFS=$oldIFS; # Limit scope of options ( set -e; foo; bar; baz; )