Thuta Learning
AdvancedDevOps & Toolsbeginner

Subshells and Process Substitution

What you'll walk away with

  • Explain the core ideas behind Subshells and Process Substitution
  • Run the sample code and verify its output
  • Apply the technique correctly to the Tutorial Platform and production scenarios

Build the mental model

A subshell is a forked child copy of the current shell process — cheap to create, but a fully separate process with its own private copy of every variable, working directory, and trap. Bash creates one two ways: explicitly, whenever you wrap commands in (...), and implicitly, for every command that sits on either side of a pipe (command substitution $(...) also runs in a subshell, for the same reason). Whatever a subshell changes — a variable's value, a cd, a trap registration — disappears the moment it exits; the parent shell that spawned it never sees the change, because the two were never sharing memory in the first place. This explains a very common surprise: piping into a while read loop, like cat file | while read -r line; do total=$((total+1)); done, silently loses total after the loop, because the loop is the right-hand side of a pipe and therefore ran in its own subshell the whole time — the moment the pipe closes, that subshell's total, and every other variable it touched, is gone. (Bash does have a shopt -s lastpipe option that lets the last command of a pipeline run in the current shell instead, but it's off by default and mostly relevant to non-interactive scripts, so don't rely on it without turning it on explicitly.) Process substitution, <(command) and >(command), sidesteps the whole problem: it turns a command's output into something that looks like a file path bash can redirect from or into directly, with no pipe involved, so a loop fed this way runs in your actual current shell and every variable it sets sticks around afterward.

Connect it to a real scenario

Say you're summing byte counts from a log-processing pipeline and need the final total once the loop finishes — maybe to compare against a threshold or log a report. Writing it the obvious way, generate_sizes | while read -r size; do total=$((total+size)); done; echo "$total", always prints 0 (or whatever total was before the loop), no matter how many lines generate_sizes produced. The reason is exactly the subshell trap above: the while loop sits on the right side of the pipe, so bash runs it in a forked subshell that has its own private total, and every increment inside the loop only ever updates that private copy. Once the pipe closes and the subshell exits, the copy is thrown away, and the total variable back in your actual shell was never touched at all — it's still whatever it was set to before the loop started (usually 0). Switching to process substitution instead, while read -r size; do total=$((total+size)); done < <(generate_sizes), fixes it completely: the loop is no longer on either side of a pipe, so no subshell gets created, and every increment to total happens directly in your current shell. By the time the loop ends, total holds the real sum, because there was only ever one copy of it the whole time.

Try the working example

bash
#!/bin/bash
set -euo pipefail

count=0

echo "--- a subshell (...) runs in a child process ---"
(
  count=99
  echo "inside subshell, count is: $count"
)
echo "outside subshell, count is still: $count"

echo "--- piping into 'while read' also runs the loop in a subshell ---"
count=0
printf "1\n2\n3\n" | while read -r n; do
  count=$((count + n))
  echo "inside the pipe's while loop, running total: $count"
done
echo "outside the loop, count is still: $count (the total was lost!)"

echo "--- process substitution avoids the pipe, so no subshell is created for the loop ---"
count=0
while read -r n; do
  count=$((count + n))
  echo "inside the loop, running total: $count"
done < <(printf "1\n2\n3\n")
echo "outside the loop, count is now: $count (the total survived)"
You should see
Running the script above prints:

--- a subshell (...) runs in a child process ---
inside subshell, count is: 99
outside subshell, count is still: 0
--- piping into 'while read' also runs the loop in a subshell ---
inside the pipe's while loop, running total: 1
inside the pipe's while loop, running total: 3
inside the pipe's while loop, running total: 6
outside the loop, count is still: 0 (the total was lost!)
--- process substitution avoids the pipe, so no subshell is created for the loop ---
inside the loop, running total: 1
inside the loop, running total: 3
inside the loop, running total: 6
outside the loop, count is now: 6 (the total survived)

5-minute try-it

Write a script that reads a list of numbers with while read, once piped from a command (losing the total) and once via process substitution (keeping it). Compare the final printed total in both versions to confirm the difference yourself.

One important caution

Expecting a variable set inside cmd | while read ...; done to still be set after the loop — it's silently reset because the loop ran in a subshell

Confusing <(...) process substitution with $(...) command substitution — <(...) produces a file-like path for redirection, while $(...) captures output as a string

GNU Bash Manual — Process SubstitutionBash / Shell Scripting

Easy traps

  • Expecting a variable set inside cmd | while read ...; done to still be set after the loop — it's silently reset because the loop ran in a subshell
  • Confusing <(...) process substitution with $(...) command substitution — <(...) produces a file-like path for redirection, while $(...) captures output as a string
  • Validate sample code in a local or test environment before applying it to a production system.

Exercise

Write a script that reads a list of numbers with while read, once piped from a command (losing the total) and once via process substitution (keeping it). Compare the final printed total in both versions to confirm the difference yourself.

You'll know it worked when: Running the script above prints: --- a subshell (...) runs in a child process --- inside subshell, count is: 99 outside subshell, count is still: 0 --- piping into 'while read' also runs the loop in a subshell --- inside the pipe's while loop, running total: 1 inside the pipe's while loop, running total: 3 inside the pipe's while loop, running total: 6 outside the loop, count is still: 0 (the total was lost!) --- process substitution avoids the pipe, so no subshell is created for the loop --- inside the loop, running total: 1 inside the loop, running total: 3 inside the loop, running total: 6 outside the loop, count is now: 6 (the total survived)

Subshells and Process Substitution | Thuta Learning