What Is Process Substitution?
Normal piping:
cat data.txt | grep "ERROR"Grep reads from stdout. Works great.
But some commands need an actual file, not stdin. Like diff:
diff file1.txt file2.txtHow do you diff two dynamic sources?
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:
<(command)Examples:
Compare Two Dynamically Generated Lists
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
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
diff <(ssh user@server cat /etc/passwd) /etc/passwdCompare the remote passwd file to local. Perfect for auditing.
Conditional Branching on Multiple Commands
if diff -q <(ls /tmp) <(echo "myfile.txt") > /dev/null; then echo "File exists in /tmp"fiCheck 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:
>(command)Examples:
Send Output to Multiple Programs
echo "hello world" | tee >(cat > file1.txt) >(cat > file2.txt)Actually, tee does this natively. But process substitution version:
command | tee >(process1) >(process2) >(process3)Run the same data through multiple processors in parallel.
Log and Filter Simultaneously
application_output | tee >(cat > full.log) | grep "ERROR" > errors.logCapture full output to one file, errors to another.
Send Output to Remote Server
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
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
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!"fiHash every file locally and on the backup server, compare checksums.
Merge Multiple Log Streams
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
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
ssh server1 cat /etc/passwd > /tmp/p1.txtssh server2 cat /etc/passwd > /tmp/p2.txtdiff /tmp/p1.txt /tmp/p2.txtrm /tmp/p1.txt /tmp/p2.txtClunky. Creates temporary files. Easy to forget cleanup.
New Way: Process Substitution
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
ls -l <(echo "test")Output:
/dev/fd/63It’s a file descriptor, not a regular file. Most commands work fine. But some (especially older ones) don’t like it.
Test:
# This worksdiff <(echo "a") <(echo "b")
# This might not work in all contextscat <(echo "test") > output.txt # Fine, cat reads from the FIFO
# But some commands reject itgzip < <(echo "test") # Works, input redirectiongzip <(echo "test") # Might fail, expects actual fileSubshell Behavior
Variables set inside a process substitution don’t persist:
var=outerdiff <(var=inner; echo $var) <(echo $var)Each <() runs in its own subshell.
Bash Only
# Bash: worksdiff <(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 environmentsdiff \ <(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:
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 errorBottom Line
Process substitution is a power tool:
<()treats command output as a file (useful for diff, sort, paste)>()treats a command as a file destination (useful for tee, logging, archiving)
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.