Process substitution is a bash syntax that allows chaining background, and the result is sent to a linux pipe / fifo queue (but no mkfifo needed), and then a symlink for those pipes are created under the path /dev/fd/<number> (as like a file descriptor). That makes as if the main command, for example diff , is reading from files (in this case even not being regular files). All of that gets cleaned up after the process is gone, so no need to remove pipes or temp files.

So to understand clear how it works:

  1. Creates anonymous pipes (special files, mainly intended for processes to communication in a first-in-first-out manner).
  2. Gives the main command the file paths to those pipes, or a symlink to those file descriptors.
  3. Runs the commands in the background, feeding output into the pipes.

It’s nice to use as input for some commands that do expect a file instead of stdin with |, and making it possible to run those commands with a pre-sequence of commands to generate the file inputs. As an example the diff command could be one where this syntax could be valuable.

Process substitution syntax : <( command ) or >(command).

As an example what we can have these 2 files:

bat test*.csv --terminal-width 50
─────┬────────────────────────────────────────────
     │ File: test1.csv
─────┼────────────────────────────────────────────
   1 │ id,name,description
   2 │ 1,"line 1","another column for line 1"
   3 │ 2,"line 2","another column for line 2"
   4 │ 3,"line 3","another column for line 3"
─────┴────────────────────────────────────────────
─────┬────────────────────────────────────────────
     │ File: test2.csv
─────┼────────────────────────────────────────────
   1 │ id,name,desc
   2 │ 3,"line 3","another column for line 3"
   3 │ 1,"line 1","another column for line 1"
   4 │ 2,"line 2","another column for line 2"
─────┴────────────────────────────────────────────

Cleaned content of the files for copy:

test1.csv content:

id,name,description
1,"line 1","another column for line 1"
2,"line 2","another column for line 2"
3,"line 3","another column for line 3"

test2.csv content:

id,name,desc
3,"line 3","another column for line 3"
1,"line 1","another column for line 1"
2,"line 2","another column for line 2"

We can see that test1.csv and test2.csv have the same content, but they’re out of order, both have a header (with different names but same meaning) and it needs to be cut out of the comparison.

So without process substitution we could do the following:

tail -n +2 test1.csv > /tmp/t1.csv | sort && \
tail -n +2 test2.csv > /tmp/t2.csv | sort && \
diff /tmp/f1.csv /tmp/f2.csv

diff has a sort option, but it’s limited (you can’t use a delimiter and sort by another column / field), and it can’t cut the header of the file. With the above we need to run some commands in sequence to generate the files, and then compare with diff. In this case we have to manage some clean up after the process runs, like removing the /tmp/ files, and the command itself becomes a long sequence and chain of pipes.

Now with the process substitution we have the following:

diff <(tail -n +2 test1.csv | sort) <(tail -n +2 test2.csv | sort)

Looks way simpler and cleaner, and all file descriptors, pipes related to the background processes will be removed after their execution, so no extra cleanup process.

You can check the background processes running, just make your processes run long enough for checking on it:

diff <(sleep 300; tail -n +2 test1.csv | sort) <(sleep 300; tail -n +2 test2.csv | sort)

You can see the background processes running with pstree for example: Ps: the $() chain is to get the parent of the diff process, so in this case 14705.

pstree -ap $(ps -ef | grep diff | grep -v "grep" | awk '{print $3}')
bash,14705
  |-bash,14933
  |   `-sleep,14936 300
  |-bash,14934
  |   `-sleep,14937 300
  |-diff,14935 /dev/fd/63 /dev/fd/62
  |-sleep,14920 300
  `-sleep,14921 300

And you can see the pipes and files under the process as well:

[root@alarm alarm]# ls -l /proc/14935/fd/
total 0
lrwx------ 1 root root 64 Jan 30 11:16 0 -> /dev/pts/1
lrwx------ 1 root root 64 Jan 30 11:16 1 -> /dev/pts/1
lrwx------ 1 root root 64 Jan 30 11:16 2 -> /dev/pts/1
lr-x------ 1 root root 64 Jan 30 11:16 3 -> 'pipe:[149924]'
lr-x------ 1 root root 64 Jan 30 11:16 4 -> 'pipe:[149926]'
lr-x------ 1 root root 64 Jan 30 11:13 62 -> 'pipe:[149926]'
lr-x------ 1 root root 64 Jan 30 11:13 63 -> 'pipe:[149924]'
[root@alarm alarm]# lsof -p 14935 | grep pipe
diff    14935 root   3r  FIFO   0,14      0t0 149924 pipe
diff    14935 root   4r  FIFO   0,14      0t0 149926 pipe
diff    14935 root  62r  FIFO   0,14      0t0 149926 pipe
diff    14935 root  63r  FIFO   0,14      0t0 149924 pipe

You can see that diff thinks it’s reading from a regular file as:

  |-diff,14935 /dev/fd/63 /dev/fd/62

And we can see the fd file descriptor as like a symlink to the queues.

As another way to visualize how it works you can run the following:

[root@alarm alarm]# ls -ltr <(echo "hello")
lr-x------ 1 root root 64 Jan 30 11:50 /dev/fd/63 -> 'pipe:[152163]'

Here we can see that <(echo "hello") is indeed a symlink under /dev/fd, pointing to a queue that will generate the result.

Another example could be like doing diff agains two dir ls, to compare files:

diff <(ls /dir1) <(ls /dir2)

By the way you can write to the substitution too:

bat test3.txt --terminal-width 50
─────┬────────────────────────────────────────────
     │ File: test3.txt
─────┼────────────────────────────────────────────
   1 │ A
   2 │ B
   3 │ C
─────┴────────────────────────────────────────────

test3.txt content:

A
B
C

Getting lines with A to file A.out, B to B.out :

cat test3.txt | tee >(grep A > A.out) >(grep B > B.out) > /dev/null

Ps: /dev/null is there just for tee to run on some sort of silent mode, not showing the initial cat result as well.

bat A.out B.out --terminal-width 50
─────┬────────────────────────────────────────────
     │ File: A.out
─────┼────────────────────────────────────────────
   1 │ A
─────┴────────────────────────────────────────────
─────┬────────────────────────────────────────────
     │ File: B.out
─────┼────────────────────────────────────────────
   1 │ B
─────┴────────────────────────────────────────────

Most modern kernels support process substitution, and the main though is whenever you face a problem where you would need to use temp files, process substitution might be a clever way to achieve the same thing, with less cleanup and cleaner bash code.

More references:

Updated: