127

I was wondering how to understand the following:

Piping the stdout of a command into the stdin of another is a powerful technique. But, what if you need to pipe the stdout of multiple commands? This is where process substitution comes in.

In other words, can process substitution do whatever pipe can do?

What can process substitution do, but pipe cannot?

Tim
  • 101,790

6 Answers6

200

A good way to grok the difference between them is to do a little experimenting on the command line. In spite of the visual similarity in use of the < character, it does something very different than a redirect or pipe.

Let's use the date command for testing.

$ date | cat
Thu Jul 21 12:39:18 EEST 2011

This is a pointless example but it shows that cat accepted the output of date on STDIN and spit it back out. The same results can be achieved by process substitution:

$ cat <(date)
Thu Jul 21 12:40:53 EEST 2011

However what just happened behind the scenes was different. Instead of being given a STDIN stream, cat was actually passed the name of a file that it needed to go open and read. You can see this step by using echo instead of cat.

$ echo <(date)
/proc/self/fd/11

When cat received the file name, it read the file's content for us. On the other hand, echo just showed us the file's name that it was passed. This difference becomes more obvious if you add more substitutions:

$ cat <(date) <(date) <(date)
Thu Jul 21 12:44:45 EEST 2011
Thu Jul 21 12:44:45 EEST 2011
Thu Jul 21 12:44:45 EEST 2011

$ echo <(date) <(date) <(date)
/proc/self/fd/11 /proc/self/fd/12 /proc/self/fd/13

It is possible to combine process substitution (which generates a file) and input redirection (which connects a file to STDIN):

$ cat < <(date)
Thu Jul 21 12:46:22 EEST 2011

It looks pretty much the same but this time cat was passed STDIN stream instead of a file name. You can see this by trying it with echo:

$ echo < <(date)
<blank>

Since echo doesn't read STDIN and no argument was passed, we get nothing.

Pipes and input redirects shove content onto the STDIN stream. Process substitution runs the commands, saves their output to a special temporary file and then passes that file name in place of the command. Whatever command you are using treats it as a file name. Note that the file created is not a regular file but a named pipe that gets removed automatically once it is no longer needed.

Caleb
  • 70,105
  • 2
    If I understood corretly, http://tldp.org/LDP/abs/html/process-sub.html#FTN.AEN18244 says process substitution creates temporary files, not named pipes. As far as I know named do not create temporary files. Writing to the pipe never involves writing to disk: https://stackoverflow.com/a/6977599/788700 – Adobe Dec 17 '17 at 13:13
  • 1
    I know this answer is legit 'cause it uses the word grok :D – aqn Nov 01 '18 at 13:46
  • 4
    @Adobe you can confirm whether the temporary file process substitution produces is a named pipe with: [[ -p <(date) ]] && echo true. This produces true when I run it with bash 4.4 or 3.2. – De Novo Jan 28 '19 at 19:56
  • 1
    Good answer, except "to a temporary file and then" … You mention that it is not really a temporary file it is a pipe. However is in not "and then". Because it is a pipe the command (cat in your example) can be started before the sub-commands are started (date in your example). Start order is not defined. – ctrl-alt-delor Feb 24 '21 at 11:41
  • 3
    @ctrl-alt-delor I agree that wording isn't great, but I'm glossing over the implementation details of FIFOs. Technically it isn't a "file", but it is a FIFO node with a file system path and it does get created first, then other parts of the pipieline are run. The same is true of pipes, it just so happens that the FIFO they are given is a standard stream name. Happy to accept edits that keep the explanation simple and clear without actually saying anything wrong. – Caleb Feb 25 '21 at 10:47
  • For echo you can use echo < <(date >/dev/stdin) – kenn Mar 26 '22 at 17:27
  • @kenn: That’s a very unconventional and unintuitive command.   What is it supposed to do?   What is it supposed to illustrate? – G-Man Says 'Reinstate Monica' Jul 07 '22 at 17:22
  • Another way to see how your shell is implementing process redirection is to do ls -l <(date) and ls -lL <(date).  And/or stat <(date) and stat -L <(date). – G-Man Says 'Reinstate Monica' Jul 07 '22 at 19:09
  • @G-ManSays'ReinstateMonica' my comment was related to echo <(date) in the answer. – kenn Jul 10 '22 at 15:25
  • @kenn: Well, I guessed that your comment was intended to be related to the answer.  But there’s a million things that you could have said, like echo <(date +'%b %e'), that would not have contributed to the discussion.  And I don’t understand the point of your comment.  So, can you explain what your command is supposed to illustrate? – G-Man Says 'Reinstate Monica' Jul 10 '22 at 17:48
  • I'll just mention that Advanced Bash Scripting guide at tldp.org, mentioned by @Adobe, is generally not considered a good source. It's better to use the official documentation, or e.g. http://mywiki.wooledge.org/BashGuide. – Carl Sep 10 '22 at 19:03
  • As Carl just noted @Adobe, there are quite a few mistakes on that site and the particular statement you cite is demonstrably wrong. You can easily prove it to yourself by using the file command to show the type of thing that gets created. Try file <(echo foo) and it will tell you "symbolic link to pipe". – Caleb Sep 12 '22 at 07:35
45

Here are three things you can do with process substitution that are impossible otherwise.

Multiple process inputs

diff <(cd /foo/bar/; ls) <(cd /foo/baz; ls)

There simply is no way to do this with pipes.

Preserving STDIN

Say you have the following:

curl -o - http://example.com/script.sh
   #/bin/bash
   read LINE
   echo "You said ${LINE}!"

And you want to run it directly. The following fails miserably. Bash is already using STDIN to read the script, so other input is impossible.

curl -o - http://example.com/script.sh | bash 

But this way works perfectly.

bash <(curl -o - http://example.com/script.sh)

Outbound process substitution

Also note that process substitution works the other way too. So you can do something like this:

(ls /proc/*/exe >/dev/null) 2> >(sed -n \
  '/Permission denied/ s/.*\(\/proc.*\):.*/\1/p' > denied.txt )

That's a bit of a convoluted example, but it sends stdout to /dev/null, while piping stderr to a sed script to extract the names of the files for which a "Permission denied" error was displayed, and then sends THOSE results to a file.

Note that the first command and the stdout redirection is in parentheses (subshell) so that only the result of THAT command gets sent to /dev/null and it doesn't mess with the rest of the line.

tylerl
  • 2,468
  • 15
  • 18
  • It's worth noting that in the diff example you might want to care about the case where the cd might fail: diff <(cd /foo/bar/ && ls) <(cd /foo/baz && ls). – phk Mar 06 '16 at 12:03
  • "while piping stderr": isn't the point that this is not piping, but going through a fifo file? – Gauthier Aug 25 '16 at 07:59
  • @Gauthier no; the command gets substituted not with a fifo but with a reference to the file descriptor. So "echo <(echo)" should yield something like "/dev/fd/63", which is a special character device that reads or writes from FD number 63. – tylerl Aug 26 '16 at 02:40
  • The outbound example can actually be rewritten without process substitution. – Johan Boulé Jan 16 '22 at 14:12
  • @JohanBoulé What would it look like? – tylerl Jan 19 '22 at 19:23
  • 1
    I think this does the same: ls /proc/*/exe 2>&1 1> /dev/null | sed -n '/Permission denied/ s/.*\(\/proc.*\):.*/\1/p' > denied.txt – Johan Boulé Jan 20 '22 at 19:22
  • @Gauthier: Piping is “going through a fifo file”.  Why do you believe that they are different? – G-Man Says 'Reinstate Monica' Jul 07 '22 at 17:29
  • Of course output process substitution can be useful when you want to send output streams to multiple outputs, like command > >(mail usher) 2> >(mail allen) or command 2> >(mail allen) | mail usher. – G-Man Says 'Reinstate Monica' Jul 07 '22 at 19:27
32

I should suppose you are talking about bash or some other advanced shell, because the posix shell does not have process substitution.

bash manual page reports:

Process Substitution
Process substitution is supported on systems that support named pipes (FIFOs) or the /dev/fd method of naming open files. It takes the form of <(list) or >(list). The process list is run with its input or output connected to a FIFO or some file in /dev/fd. The name of this file is passed as an argument to the current command as the result of the expansion. If the >(list) form is used, writing to the file will provide input for list. If the <(list) form is used, the file passed as an argument should be read to obtain the output of list.

When available, process substitution is performed simultaneously with parameter and variable expansion, command substitution, and arithmetic expansion.

In other words, and from a practical point of view, you can use an expression like the following

<(commands)

as a file name for other commands requiring a file as a parameter. Or you can use redirection for such a file:

while read line; do something; done < <(commands)

Turning back to your question, it seems to me that process substitution and pipes have not much in common.

If you want to pipe in sequence the output of multiple commands, you can use one of the following forms:

(command1; command2) | command3
{ command1; command2; } | command3

but you can also use redirection on process substitution

command3 < <(command1; command2)

finally, if command3 accepts a file parameter (in substitution of stdin)

command3 <(command1; command2)
enzotib
  • 51,661
  • so <() and < <() makes same effect, right? – solfish Aug 17 '17 at 12:26
  • 1
    @solfish: not exacllty: the firse can be used wherever a filename is expected, the second is an input redirection for that filename – enzotib Aug 24 '17 at 20:46
  • (1) How can you say “process substitution and pipes have not much in common” when A | B and B < <(A) are equivalent?  (2) Why would somebody choose to use B < <(A) rather than A | B? – G-Man Says 'Reinstate Monica' Jul 07 '22 at 17:41
  • @G-ManSays'ReinstateMonica': also cat fname | B and B < fname are equivalent, but this doen't mean pipe and redirection are the same. One could choose B < <(A) when B is a while loop declaring some variables (see http://mywiki.wooledge.org/BashPitfalls#grep_foo_bar_.7C_while_read_-r.3B_do_.28.28count.2B-.2B-.29.29.3B_done) – enzotib Jul 15 '22 at 18:26
  • Yes, precisely!  Or, to be more precise, in most shells, A | B (or, in general, A | B | C | D …) will run each command in a separate process, even if they’re built-in commands — even if they all* are.  (Some shells will run the last* command in the main process; in some shells, this is available as an option.)  But B < <(A) will run B in the main shell process if it’s a built-in.  I was trying to prod you into adding that to your answer (as an [edit] rather than a comment). – G-Man Says 'Reinstate Monica' Jul 15 '22 at 20:43
12

If a command takes a list of files as arguments and processes those files as input (or output, but not commonly), each of those files can be a named pipe or /dev/fd pseudo-file provided transparently by process subsitution:

$ sort -m <(command1) <(command2) <(command3)

This will "pipe" the output of the three commands to sort, as sort can take a list of input files on the command line.

camh
  • 39,069
8

It should be noted that process substitution is not limited to the form <(command), which uses the output of command as a file. It can be in the form >(command) which feeds a file as the input to command as well. This is also mentioned in the quote of bash manual in @enzotib's answer.

For the date | cat example above, a command that uses the process substitution of the form >(command) to achieve the same effect would be,

date > >(cat)

Note that the > before >(cat) is necessary. This can again be clearly illustrated by echo as in @Caleb's answer.

$ echo >(cat)
/dev/fd/63

So, without the extra >, date >(cat) would be the same as date /dev/fd/63 which will print a message to stderr.

Suppose you have a program that only takes filenames as parameters and does not process stdin or stdout. I will use the oversimplified script psub.sh to illustrate this. The content of psub.sh is

#!/bin/bash
[ -e "$1" ] && [ -e "$2" ] && awk '{print $1}' < "$1" > "$2"

Basically, it tests that both of its arguments are files (not necessarily regular files) and if this is the case, write the first field of each line of "$1" to "$2" using awk. Then, a command that combines all that mentioned so far is,

./psub.sh <(printf "a a\nc c\nb b") >(sort)

This will print

a
b
c

and is equivalent to

printf "a a\nc c\nb b" | awk '{print $1}' | sort

but the following will not work, and we have to use process substitution here,

printf "a a\nc c\nb b" | ./psub.sh | sort

or its equivalent form

printf "a a\nc c\nb b" | ./psub.sh /dev/stdin /dev/stdout | sort

If ./psub.sh also reads stdin besides what is mentioned above, then such an equivalent form does not exist, and in that case there are nothing we can use instead of the process substitution (of course you can also use a named pipe or temp file, but that's another story).

Weijun Zhou
  • 3,368
2

I feel pipeline is good at gradually processing data step by step by sequence the commands one by one; process substitution is good at getting outputs from many command sequences or provide inputs to many command sequences by files; combine them appropriately could be some powerful tools.

Pipeline use a pipe to link two file descriptors, typically standard output file descriptor of a command, and standard input file descriptor of the next command, thus the output of a command can be accessed by the command next to it. You can sequence infinite commands in this way to gradually process the data.

ls | head -3 | tail -1

The output of ls is used as the input of head; the output of head is used as the input of tail.

Process Substitution creates a file by which other commands can access the output of a command sequence or provide input to it.

paste <(echo hello) <(echo world) | tee >(cat) >(cat)

<(echo hello) and <(echo world) created two files as the input of paste, one file includes 'hello', one includes 'world', the output of the echo commands;

tee reads the output ('hello world') of paste through the pipe;

the two >(cat) commands also create two files as the input of the two cat commands, two files all include 'hello world' which is written by the command tee;

then the two cat commands output the content of the files they get as their input.

The output is as below:

# paste <(echo hello) <(echo world) | tee >(cat) >(cat)
hello   world
hello   world
hello   world

First 'hello world' is output of the paste command; The two after it is output of the two cat commands.

The file is created by <() includes output of the command sequence as its content; but the file created by >() is empty, you must write something to it explicitly.

➜  Downloads echo hello >(cat)
hello /proc/self/fd/12
➜  Downloads echo hello > >(cat)
hello

Above, first time the 'hello' is produced by the echo command; because the file created by >(cat) is empty, so the name of the file is produced by the shell.

Second time, the 'hello' is redirected to the file created by >(cat), then the file is taken as the input of the cat command which produces the 'hello'.

More examples:

comm -23 <(set -o posix; set | sort) <(env | sort)

Comparing the difference between shell variables and environmental variables.

References:

https://www.gnu.org/software/bash/manual/html_node/Pipelines.html

https://www.gnu.org/software/bash/manual/html_node/Process-Substitution.html

https://man7.org/linux/man-pages/man2/pipe.2.html

Lee Li
  • 241