Subshells, grouping and process substitution

Bash · Advanced · 12 min

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=1

Remember

  • ( ) 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 read and reading it after the loop.

Check yourself

  1. Why does `echo hi | read -r v; echo "$v"` print an empty line in bash by default?
  2. Which grouping runs commands in the current shell?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.gnu.org/software/bash/manual/bash.html#Process-Substitution