Bash Process Substitution: <() and >()

Tested on RHEL 10.2 (Coughlan)
Package bash 5.2.26-6.el10
Applies to Linux distributions with Bash installed
Privilege Normal user; examples create files in the current directory
Scope Bash process substitution with <(command) and >(command): file-like paths, /dev/fd or FIFO implementation, comparison with pipes and command substitution, while read in the current shell, diff, tee fan-out, exit status, and portability. Does not cover named custom file descriptors, full pipeline/pipefail reference, or heredocs.
Related guides Shell scripting course hub
Short on time?Beta Get the gist of this 10-minute read in seconds, then ask follow-up questions

Bash process substitution exposes a command's input or output through a file-like pathname. <(command) lets another command read the output as if it were a file, while >(command) lets another command write into a process.

For example, diff <(sort file1) <(sort file2) compares two command outputs without manually creating and cleaning up temporary data files. Process substitution is also useful with while read loops and tee, but it is Bash-specific and its asynchronous behavior matters when exit status or completion must be tracked.


What Is Process Substitution?

Process substitution is a Bash (and ksh/zsh) feature — not POSIX sh. It is useful when a command expects a pathname or multiple independent input sources, but the data you want to provide comes from other commands.

Form What Bash gives the surrounding command
<(list) A pathname whose contents are the stdout of list
>(list) A pathname you can write to; Bash forwards that data to stdin of list

The substituted process runs asynchronously. Bash connects it through a FIFO or a /dev/fd descriptor, depending on the system. There must be no space between < or > and ( — < (cmd) is a syntax error, not process substitution.


<(command): Use Output as a File

<(command) expands to a pathname such as /dev/fd/63. Commands that accept file operands, such as diff, comm, paste, grep, and wc, can read from <(command) directly.

Compare two sorted lists without manually creating temporary data files:

bash
printf 'b\na\nc\n' > a.txt
printf 'c\nb\na\n' > b.txt
diff <(sort a.txt) <(sort b.txt)

An empty diff means the sorted contents match. Bash runs both sort commands asynchronously and gives diff a separate file-like pathname for each output.

Inspect the generated pathname (the number varies by run and shell):

bash
bash -c 'printf "path=%s\n" <(printf hello)'
output
path=/dev/fd/63

Do not hard-code /dev/fd/63 in scripts. Treat the expansion as an opaque handle Bash creates for one command line.


>(command): Send Output to a Process

>(command) gives you a pathname to write to. Data written there is piped into the substituted command's stdin.

Send a line through tee into a file without a named pipe on disk:

bash
rm -f recv.log
printf 'stream data\n' > >(tee recv.log)
output
stream data

tee receives the data through its stdin and writes a copy to recv.log while also printing it to stdout. Because the process substitution runs asynchronously, do not rely on recv.log being complete immediately after the surrounding command finishes.

Use >(command) when the surrounding command needs an output pathname, but you want data written to that pathname to become another command's stdin.


How /dev/fd or FIFOs Make It Work

GNU Bash documents process substitution as connecting the substituted list to a FIFO or a file descriptor under /dev/fd. From your script's perspective, the expansion is just a pathname string.

That is why diff <(cmd1) <(cmd2) works: diff receives two pathname arguments and reads from them like files. The exact mechanism is system-dependent; Bash may use named pipes or /dev/fd, so scripts should treat the generated pathname as an opaque, temporary interface rather than depend on its exact form.


Process Substitution vs Pipe

A pipe connects stdout of one command directly to stdin of the next. Process substitution exposes a path instead.

Mechanism Best when
Pipe cmd1 | cmd2 One stream flows straight into the next program
<(cmd) A command needs a file argument, or you want redirection instead of a pipeline
>(cmd) A command needs to write to a path that feeds another process

diff is a classic use case because it normally compares two file operands, while a pipe provides only one stdin stream. Process substitution gives diff two separate file-like pathnames without manually creating and cleaning up temporary data files. By default, Bash executes pipeline elements in separate subshell environments, which is why variables changed inside cmd | while read ... usually disappear afterward — see Bash pipes for pipefail and PIPESTATUS. Process substitution avoids a subshell for the reader when you use redirection (< <(cmd)) instead of a pipe.


Process Substitution vs Command Substitution

Command substitution captures stdout into a string with $(command). Process substitution exposes stdout as a file-like path with <(command).

Both can feed wc -l, but they behave differently behind the scenes:

bash
printf 'line1\nline2\n' > sample.txt

Count lines from a captured string (here-string):

bash
wc -l <<< "$(cat sample.txt)"
output
2

Count lines from process substitution (streaming file descriptor):

bash
wc -l < <(cat sample.txt)
output
2

Use $(command) when you need the text in a variable or as part of a string. Use <(command) when a tool should read from a file and you want streaming input — or when trailing-newline rules from command substitution would get in the way.

Approach Data handling
$(command) Captures the command's output as shell text before use
<(command) Streams data through a FIFO or file descriptor to the reader
Temp file Stores data on disk

Prefer process substitution for large streaming output when the consumer can read incrementally.


Keep a while read Loop in the Current Shell

A common pipeline pitfall: because pipeline elements run in subshells by default, variables set inside cmd | while read usually disappear afterward.

bash
count=0
printf 'one\ntwo\nthree\n' | while IFS= read -r line; do count=$((count + 1)); done
printf 'pipe count=%s\n' "$count"
output
pipe count=0

Process substitution keeps the loop in the current shell because the input arrives through redirection, not a pipe:

bash
count=0
while IFS= read -r line; do count=$((count + 1)); done < <(printf 'one\ntwo\nthree\n')
printf 'procsub count=%s\n' "$count"
output
procsub count=3

What does < <(command) mean?

The two < characters belong to different constructs. <(command) is process substitution: Bash runs command asynchronously and produces a file-like pathname such as /dev/fd/63. The separate < before it is normal input redirection.

Conceptually, done < <(command) becomes something like done < /dev/fd/63. The loop therefore reads the substituted command's output through stdin while the loop itself remains in the current shell.

The same pattern is covered in read a file line by line. For loading all lines into an array at once, mapfile with < <(command) is often simpler. When each incoming line becomes a lookup key rather than an ordered item, the Bash associative-array guide uses the same done < <(command) shape to consume sorted map keys.


Compare Two Command Outputs with diff

Sorting before diff is a practical pattern when order differs but content should match:

bash
diff <(sort a.txt) <(sort b.txt)
echo "diff exit=$?"
output
diff exit=0

When the files differ, diff prints the differences and exits with status 1. The exit status you care about in scripts is usually diff's status, not the substituted commands' statuses — see the exit-status section below.


Fan Out with tee and >(...)

tee can write to multiple destinations. Process substitution lets one destination be another command:

bash
rm -f payload.gz sha.txt
printf 'payload data\n' | tee >(gzip -c >payload.gz) >(sha256sum >sha.txt)
output
payload data

tee prints the line to the terminal while also sending a copy to each >(...) branch. This demonstrates fan-out, but the two consumers run asynchronously. Do not immediately depend on payload.gz or sha.txt unless you add explicit synchronization outside process substitution — the tee pipeline can finish before gzip or sha256sum has finished writing those files.


Exit Status and Asynchronous Behavior

Process substitution does not turn the substituted command's exit status into the main command's exit status automatically.

When one substituted command fails but diff still produces output, you see diff's status — not a direct report of the inner failure:

bash
diff <(false) <(printf 'x\n') 2>/dev/null
echo "main exit=$?"
output
0a1
> x
main exit=1

Here main exit=1 comes from diff, not from false. Check substituted commands explicitly when their success matters — run critical commands outside process substitution, or restructure the workflow when you need reliable status from each branch.

Process-substitution commands run asynchronously, and $? normally reports the status of the surrounding command, not the commands inside <(...) or >(...). A plain wait is not a general solution for tracking multiple process substitutions. If individual completion or exit status matters, use a design with explicit background jobs, a normal pipeline with pipefail/PIPESTATUS, or another synchronization mechanism.


Portability to sh

Process substitution is a Bash extension — POSIX sh does not define <(list) or >(list). A script may appear to work when sh happens to invoke a shell that supports the extension, so do not use runtime success as proof of POSIX compatibility.

If the script uses <(...) or >(...), declare Bash explicitly with #!/usr/bin/env bash or #!/bin/bash. For portability notes beyond Bash, see Bash vs sh. When sh compatibility is required, use a temporary file or a pipe redesign instead.


Common Syntax and Lifecycle Mistakes

Symptom / trap Likely cause Fix
syntax error near ( Space before ( — < (cmd) Write <(cmd) or >(cmd) with no space
Works in Bash but fails under sh Process substitution is not POSIX Run with Bash shebang or avoid <()/>()
Loop counter stays zero after while read Pipeline subshell Use done < <(command) instead of | while read
Assumed inner command's $? is the script's $? Substituted process runs asynchronously Check the main command's $?; restructure when inner status matters
Hard-coded /dev/fd/63 breaks FD number is not stable Use <(cmd) expansion; never embed the path literally
>(gzip …) output missing or incomplete Async consumer still running; bare wait may not sync all branches Do not read output files immediately; use explicit jobs or another coordination pattern

Summary

Bash process substitution exposes command I/O through pathnames: <(command) for file-like input to tools such as diff, and >(command) for file-like output into another process. It fills the gap between pipes (stream to stream) and $(command) (capture as text) without requiring you to manage temporary data files yourself.

Use < <(command) when a while read loop must update variables in the current shell. Use tee >(cmd1) >(cmd2) to fan out streams, but remember substituted processes run asynchronously — their exit statuses are not automatically your script's exit status, and a bare wait does not reliably synchronize multiple branches.

Process substitution requires Bash (or ksh/zsh), not POSIX sh. Pair it with Bash redirection when you need stderr merged or log files alongside file-like paths.


References


Frequently Asked Questions

Why can't there be a space before (?

< and > are redirection operators. Process substitution requires the token <( or >( as a single unit. A space — < (cmd) — is parsed as input redirection followed by a subshell, which is invalid in that position.

Is process substitution POSIX?

No. It is defined by Bash and also available in ksh and zsh. POSIX sh and dash reject <(…).

Does process substitution run asynchronously?

Yes. Bash runs the substituted command asynchronously and connects it through a FIFO or /dev/fd while the surrounding command executes.

How do I get its exit status?

The exit status of the main command (for example diff or tee) is what $? reports immediately afterward. To capture a substituted command's status, run it outside process substitution, start it as an explicit background job you can wait on, or inspect PIPESTATUS when the design uses a normal pipeline. A bare wait after multiple >(...) branches does not reliably report every consumer's status.

When should I use it instead of a pipe?

Use process substitution when a command needs a file operand (diff, comm, wc), when a while read loop must stay in the current shell (< <(cmd)), or when you want streaming input without storing $(cmd) output in a variable. Use a plain pipe when stdout of one command should flow directly into the next program's stdin.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)