Lost variable updates and puzzling cd behaviour are almost always subshell effects.
A subshell is a forked copy of the current shell. It inherits all variables (exported or not), but nothing it changes, not variables, cd, set options or traps, makes it back to the parent. Bash creates subshells for:
( commands )explicit grouping,$( command substitution ),- each part of a pipeline (all of them by default),
- background jobs
cmd &.
{ commands; } groups without a subshell: same process, so changes persist. Use it to redirect several commands at once: { date; uname -a; } > info.txt. Note the required spaces and the final ;. Use ( ) when you want isolation, such as (cd build && make) to change directory temporarily.
The classic trap is cmd | while read -r line; do count=$((count+1)); done: the loop runs in a subshell and count is unchanged afterwards. Fix it by redirecting instead of piping, using process substitution: done < <(cmd).
Process substitution <(cmd) runs cmd and gives you a file name (like /dev/fd/63) connected to its output; >(cmd) gives a file name that feeds cmd's input. It lets you pass command output to programs that insist on file arguments: diff <(sort a) <(sort b), or tee >(gzip > log.gz).
Example
(cd /tmp && pwd) # /tmp -- subshell: the cd does not leak
pwd # unchanged
{ echo one; echo two; } > /tmp/both.txt # group, same shell, one redirect
count=0
printf 'a\nb\n' | while read -r _; do count=$((count + 1)); done
echo "pipe: $count" # pipe: 0 (loop ran in a subshell)
count=0
while read -r _; do count=$((count + 1)); done < <(printf 'a\nb\n')
echo "procsub: $count" # procsub: 2
diff <(printf '1\n2\n3\n') <(printf '1\n3\n')
# 2d1
# < 2
echo <(true) # /dev/fd/63 (it is a file name)
x=1; (x=2); echo "x=$x" # x=1Remember
- ( ) and pipeline stages run in subshells; their changes are lost
- { ...; } groups in the current shell
- <(cmd) turns output into a readable file name; use done < <(cmd)
Watch out for
- Incrementing a counter inside
cmd | while readand reading it after the loop.