What Process Substitution Actually Does
Process substitution, written <(command) for input or >(command) for output, is a Bash feature that runs command in the background and exposes its stdin or stdout as a filesystem path — typically /dev/fd/N on Linux — that another command can open just like a regular file. This matters because many programs, such as diff, only accept file paths as arguments and can't read from a pipe directly; <(command) lets you hand such a program the live output of a command without ever writing an intermediate temp file. Unlike a pipe, which connects exactly two commands' stdin/stdout, process substitution produces a path usable anywhere a filename is expected, including as multiple arguments to the same command.
Cricket analogy: Process substitution is like a broadcaster feeding a live match feed into a replay system as if it were a pre-recorded file, letting the third umpire 'open' the live stream the same way they'd open a saved clip.
Common Use Case: Diffing Command Output
The canonical use of process substitution is comparing the output of two commands without saving either to a temp file first: diff <(sort file1.txt) <(sort file2.txt) runs both sorts concurrently and hands diff two live file-like paths to compare. This pattern generalizes to any command that expects file arguments — comm, join, or even feeding <(command) as the input to a script that only knows how to read from a path — and it avoids the cleanup burden and race conditions that come with manually creating and deleting mktemp temp files.
Cricket analogy: diff <(sort teamA.txt) <(sort teamB.txt) is like comparing two teams' sorted batting averages side by side without printing either list to paper first — both sorted streams are compared live.
Output Substitution and Feeding Multiple Consumers
>(command) works in the opposite direction: it creates a path that, when written to, streams data into command's stdin, which is useful for splitting one stream of output to multiple destinations, as in some_process | tee >(gzip > out.gz) >(wc -l > count.txt) > raw.log, where tee fans the same output to a compressed file, a line-counting process, and a raw log simultaneously. Because each <(...) or >(...) spawns an actual subprocess connected via a named pipe or /dev/fd entry, exit status handling is subtler than with a normal pipeline — the main command's exit code doesn't reflect failures inside a process substitution, so critical error handling there needs its own explicit checks.
Cricket analogy: tee >(gzip > out.gz) >(wc -l > count.txt) fanning one stream to three destinations is like a stadium's video feed being split simultaneously to the big screen, the broadcast truck, and the archive server from one camera source.
#!/usr/bin/env bash
set -uo pipefail
# Input process substitution: diff two live command outputs
diff <(sort users_prod.txt) <(sort users_staging.txt) > drift.diff || \
echo "Environments have drifted; see drift.diff"
# Feed a while-read loop from a command via process substitution
# (avoids the subshell variable-loss problem of piping into while read)
total=0
while IFS= read -r size; do
(( total += size ))
done < <(find . -maxdepth 1 -type f -printf '%s\n')
echo "Total bytes: $total"
# Output process substitution: fan one stream to three destinations
generate_report | tee \
>(gzip > report.gz) \
>(wc -l > report_lines.txt) \
> report.logProcess substitution requires Bash (or another shell that supports it, like zsh or ksh) — it is not part of POSIX sh, so #!/bin/sh scripts running under dash will fail with a syntax error on <(...). Always use #!/usr/bin/env bash when a script relies on this feature.
<(command)exposes a command's stdout as a readable file path;>(command)exposes a writable path feeding a command's stdin.- It solves the problem of programs like
diffthat require file arguments but need to compare live command output. - Process substitution avoids manual
mktemptemp files and their associated cleanup and race-condition risks. tee >(cmd1) >(cmd2) > filefans one stream to multiple consumers simultaneously.- Feeding
while readvia< <(command)avoids the subshell variable-loss problem that piping intowhile readcauses. - Exit status from inside a process substitution isn't automatically reflected in the main command's exit code.
- Process substitution is a Bash extension, unavailable in POSIX
/bin/sh— scripts using it need a#!/usr/bin/env bashshebang.
Practice what you learned
1. What does `<(command)` produce that another program can consume?
2. Why is `diff <(sort a.txt) <(sort b.txt)` preferred over manually creating temp files?
3. What does `>(command)` do?
4. Why must a script using process substitution use `#!/usr/bin/env bash` instead of `#!/bin/sh`?
5. What problem does `while read -r line; do ... done < <(command)` solve compared to `command | while read -r line; do ... done`?
Was this page helpful?
You May Also Like
Command Substitution in Depth
Understand how $(...) captures command output as a string, why it's preferred over backticks, and the quoting rules that prevent word-splitting bugs.
Advanced Loop Patterns
Go beyond basic for loops with C-style iteration, while-read idioms for safe line processing, and loop control with break, continue, and select.
Advanced Conditionals and Test Operators
Master Bash's test, [ ], and [[ ]] constructs with string, numeric, and file test operators to write robust, bug-free conditional logic.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics