Skip to content
Go back

Bash Process Substitution: What <() and >() Actually Do

By SumGuy 5 min read
Bash Process Substitution: What <() and >() Actually Do

What Is Process Substitution?

Normal piping:

Terminal window
cat data.txt | grep "ERROR"

Grep reads from stdout. Works great.

But some commands need an actual file, not stdin. Like diff:

Terminal window
diff file1.txt file2.txt

How do you diff two dynamic sources?

Terminal window
diff <(cat server1.log) <(cat server2.log)

<() is process substitution. It makes a command’s output look like a file. Diff sees two files, but they’re actually command output.

Input Substitution: <()

Make a command’s output look like a file:

Terminal window
<(command)

Examples:

Compare Two Dynamically Generated Lists

Terminal window
diff <(ps aux | awk '{print $1}' | sort) <(cat allowed_users.txt | sort)

Compare the running users to an allowed list. Find unexpected processes.

Pass Multiple Streams to a Program

Terminal window
sort -m <(cat file1.txt | sort) <(cat file2.txt | sort)

Merge-sort two files. sort -m expects already-sorted input files.

Feed Remote Data to a Local Command

Terminal window
diff <(ssh user@server cat /etc/passwd) /etc/passwd

Compare the remote passwd file to local. Perfect for auditing.

Conditional Branching on Multiple Commands

Terminal window
if diff -q <(ls /tmp) <(echo "myfile.txt") > /dev/null; then
echo "File exists in /tmp"
fi

Check if a file exists by comparing directory listing to expected output.

Output Substitution: >()

Feed a command’s input as if you’re writing to a file:

Terminal window
>(command)

Examples:

Send Output to Multiple Programs

Terminal window
echo "hello world" | tee >(cat > file1.txt) >(cat > file2.txt)

Actually, tee does this natively. But process substitution version:

Terminal window
command | tee >(process1) >(process2) >(process3)

Run the same data through multiple processors in parallel.

Log and Filter Simultaneously

Terminal window
application_output | tee >(cat > full.log) | grep "ERROR" > errors.log

Capture full output to one file, errors to another.

Send Output to Remote Server

Terminal window
tar czf - /data | gzip -d > >(ssh user@backup.com "cat > /backup/data.tar.gz")

Pipe a tar stream to a remote machine’s stdin as if you’re writing a file.

Multiplex Output to Multiple Commands

Terminal window
ls -la | tee >(wc -l) >(grep "^-" | wc -l) >(head -5)

Send ls output to three places: count total lines, count files, show first 5.

Real-World Examples

Compare Config Files Between Servers

Terminal window
diff <(ssh web1 cat /etc/app.conf) <(ssh web2 cat /etc/app.conf)

Spot differences without downloading files manually.

Verify Backups

#!/bin/bash
echo "Comparing local vs backup..."
diff <(find /data -type f -exec md5sum {} \; | sort) \
<(ssh backup.server find /backup/data -type f -exec md5sum {} \; | sort)
if [ $? -eq 0 ]; then
echo "Backup verified: all files match"
else
echo "Backup mismatch detected!"
fi

Hash every file locally and on the backup server, compare checksums.

Merge Multiple Log Streams

Terminal window
cat <(ssh server1 tail -f /var/log/app.log) \
<(ssh server2 tail -f /var/log/app.log) \
<(ssh server3 tail -f /var/log/app.log) | grep "ERROR"

Watch error logs from 3 servers in real-time.

Process and Archive Simultaneously

Terminal window
tar czf - /data | tee >(sha256sum > archive.sha256) | ssh backup.server "cat > data.tar.gz"

Compress, hash, and upload in one go. The hash is local, the tar goes to the server.

Comparing to Alternatives

Old Way: Temporary Files

Terminal window
ssh server1 cat /etc/passwd > /tmp/p1.txt
ssh server2 cat /etc/passwd > /tmp/p2.txt
diff /tmp/p1.txt /tmp/p2.txt
rm /tmp/p1.txt /tmp/p2.txt

Clunky. Creates temporary files. Easy to forget cleanup.

New Way: Process Substitution

Terminal window
diff <(ssh server1 cat /etc/passwd) <(ssh server2 cat /etc/passwd)

One line. No temp files. Clean.

Gotchas

Process Substitution Creates a FIFO, Not a Real File

Terminal window
ls -l <(echo "test")

Output:

/dev/fd/63

It’s a file descriptor, not a regular file. Most commands work fine. But some (especially older ones) don’t like it.

Test:

Terminal window
# This works
diff <(echo "a") <(echo "b")
# This might not work in all contexts
cat <(echo "test") > output.txt # Fine, cat reads from the FIFO
# But some commands reject it
gzip < <(echo "test") # Works, input redirection
gzip <(echo "test") # Might fail, expects actual file

Subshell Behavior

Variables set inside a process substitution don’t persist:

Terminal window
var=outer
diff <(var=inner; echo $var) <(echo $var)

Each <() runs in its own subshell.

Bash Only

Terminal window
# Bash: works
diff <(echo "a") <(echo "b")
# sh: doesn't work (sh doesn't have process substitution)

Make sure your shebang is #!/bin/bash, not #!/bin/sh.

Advanced: Combine Multiple Substitutions

#!/bin/bash
# Compare database schemas across environments
diff \
<(mysql -u user -p"$pass" dev < schema.sql | sort) \
<(mysql -u user -p"$pass" prod < schema.sql | sort)

Multiple substitutions in one command.

Track 3 log files and grep for errors:

Terminal window
tail -f <(ssh server1 tail -f /var/log/app.log) \
<(ssh server2 tail -f /var/log/app.log) \
<(ssh server3 tail -f /var/log/app.log) | grep -i error

Bottom Line

Process substitution is a power tool:

It eliminates the need for temporary files and makes bash one-liners more expressive. Once you start using it, you’ll find it everywhere.

Just remember: it’s a bash feature, not POSIX sh. And it creates FIFOs, which some old commands might reject. Test before relying on it in critical scripts.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
Bash Arrays: The Feature That Makes Scripts Readable
Next Post
MinIO + Nextcloud: S3-Compatible Storage That's Actually Yours

Related Posts