Bash – difference between cmd_drain < <(cmd_src) and cmd_drain <<< “$(cmd_src)”

bash

The cmd | read -r var1 var2 construct famously does not work in bash because the read command is executed in a subshell due to piping. I used to use read -r var1 var2 <<< "$(cmd)" to get around this, but recently I learned about the cmd_drain < <(cmd_src) construct, which seems to work just as well: read -r var1 var2 < <$(cmd).

Is there a difference between these two solutions? There does not seem to be any difference in the trivial case:

$ hd < <(echo Hello)
00000000  48 65 6c 6c 6f 0a                                 |Hello.|
00000006
$ hd <<< $(echo Hello)
00000000  48 65 6c 6c 6f 0a                                 |Hello.|
00000006

I also tried some special characters and got the same results. My gut feeling is that the result will always be the same expect that cmd_drain <<< "$(cmd_src)" will first run cmd_src and buffer the whole result in memory before feeding it to cmd_drain, while cmd_drain < <(cmd_src) will continously feed the output of cmd_src into cmd_drain. I assume it behaves like cmd_src | cmd_drain except that cmd_src will be run in a sub-shell instead of cmd_drain. Is my assumption correct?

Bonus question: Is quoting necessary around the $() construct?

Best Answer

Yes, your assumption is correct. In cmd_drain < <(cmd_src) (aka Process Substitution, combined with normal redirection), Bash will replace <(cmd_src) with the path to a file, from which the output of cmd_src can be read. From the docs:

The process list is run asynchronously, and its input or output appears as a filename. This filename 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.

In cmd_drain <<< "$(cmd_src)", <<< ... is treated like any other here-string, so:

The word undergoes tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal. Pathname expansion and word splitting are not performed. The result is supplied as a single string, with a newline appended, to the command on its standard input [...]

So you don't need to quote $() there, but specifically because the here string <<< syntax doesn't do word splitting or filename expansion. Usually, you'd have to.


Note again the last sentence of the here string documentation - a newline is appended:

bash-5.0$ od -c <<< $(printf %s foo)
0000000   f   o   o  \n
0000004
bash-5.0$ od -c < <(printf %s foo)
0000000   f   o   o
0000003

Whether or not that matters is up to what you're running.

In hd <<< $(echo Hello), the command substitution removes the trailing newline output by echo, and the here string adds a newline, effectively giving you the same output. But, as the above example shows, this removal/addition of newlines can be tricky, and you need not get exactly what cmd_src output.

Related Question