Sample output
$ diff <(sort packages-web01.txt) <(sort packages-web02.txt)
1a2
> docker
5d5
< postgres
$ diff packages-web01.txt packages-web02.txt
2,3c2
< redis
< postgres
---
> git
4a4
> docker
7c7
< git
---
> redis
The first run gives you the answer: web02 has docker and is missing postgres, and nothing else differs. The second run is the same two files compared without sorting, and it reports four hunks of pure noise, because the packages were recorded in install order rather than alphabetically. Neither machine changed between the two commands. Only the ordering did.
When you would reach for it
Two machines behave differently and you want to know what is actually installed on one and not the other. Two config dumps, two API responses, two directory listings, two git show outputs from different commits. The shape is always the same: the thing you want to compare is the output of a command, not a file sitting on disk, and the tool you want to compare it with insists on filenames. Process substitution closes that gap without leaving /tmp/a.txt and /tmp/b.txt behind for you to forget about.
Gotchas
/bin/shrejects the syntax outright. The feature is not in POSIX, so dash, the default/bin/shon Debian and Ubuntu, answers withdash: 1: Syntax error: "(" unexpected, and busyboxash, the default shell on Alpine, rejects it too. The error names a parenthesis, not a missing feature, which sends people looking for a typo. A script that works when you run it interactively and fails under cron or CI is usually this: change the shebang to#!/usr/bin/env bashand it works again.- A failure inside the substitution is invisible, and it can look like success.
diff <(cat gone-a.txt) <(cat gone-b.txt)prints two “No such file or directory” errors on stderr and then exits 0, because both pipes were empty and empty matches empty. The exit status you get belongs todiff, never to the commands inside the parentheses.set -euo pipefaildoes not catch it either, since neither substitution is part of the pipeline bash is watching. Confirm the inputs exist before you trust a clean comparison. - What you get is a pipe, not a file, and tools that seek will say so.
unzip -l <(cat archive.zip)reportsEnd-of-central-directory signature not found, because unzip needs to jump to the end of the archive and a pipe only moves forward. Anything that reads front to back is fine; anything that needs random access wants a real file. The same limit explains why you cannot save the path for later:f=<(sort packages-web01.txt)completes, but the descriptor closes with the assignment, and reading$fon the next line gives/dev/fd/63: No such file or directory. - The output form
>(cmd)runs asynchronously and the shell does not wait for it.printf 'a\nb\nc\n' > >(sleep 1; wc -l > count.txt)returns to your prompt immediately, and a script that readscount.txton the very next line finds nothing there. Bash sets$!to the substituted process, sowait $!after the command holds until it has finished writing.
Variants
$ comm -13 <(sort packages-web01.txt) <(sort packages-web02.txt)
Print only what the second list has and the first does not, with no diff markup to strip. comm reads three columns, lines unique to file one, unique to file two, and common to both, and the digits switch columns off. It also requires sorted input and will warn but still answer wrongly if you forget
$ diff <(git show HEAD~2:auth.js) auth.js
Compare a file as it looked two commits ago against what is in your working tree right now, including changes you have not staged. Mixing a substitution and a real filename in one command is allowed, since both arrive as paths
$ sort server.log | tee >(gzip > sorted.log.gz) | wc -l
The output direction: tee writes one copy into a gzip process that never touches an intermediate file, while the other copy carries on down the pipeline. Add wait before reading sorted.log.gz in a script