Capturing the process substitution exit code in Bash

Process substitutions in Bash is a very nice feature which in many cases helps to avoid temporary files. One downside of it, however, is the lack of controls for communicating the exit code of the command in a process substitution. Process substitution provides the output of a command via a file descriptor but doesn’t tell whether the output is complete or correct. Did the command produce no output or did it fail before producing any output?

There is a way to capture the exit code of the process substitution using coproc:

(
  coproc { echo Hello World; false; }
  cpid="$COPROC_PID"

  diff -du <(cat <&${COPROC[0]}) <(echo Goodbye World)

  wait "$cpid"

  echo $?
)

outputs:

--- /dev/fd/61	2022-01-18 22:36:10.201792655 +0000
+++ /dev/fd/59	2022-01-18 22:36:10.200792655 +0000
@@ -1 +1 @@
-Hello World
+Goodbye World
1

In the above example, the first process substitution forwards the output of the echo command running in a coprocess. The wait command will wait for the coprocess to finish and will exit with the exit code of the coprocess command (which ends with the false command, so the exit code is going to be 1).

It is very important to save the value of $COPROC_PID as soon as possible because it will not be available once the coprocess command has finished executing.

Also, bear in mind, that Bash (as of version 5) only supports one coprocess at a time, so it is currently not possible to apply this approach to multiple parallel process substitutions.

Similar to background processes with job control disabled, the command executing via coproc will have its standard file descriptors detached from the running terminal. Some programs are picky about that when they want to prompt on TTY (notably pinentry-curses used by gpg-agent, and by pass as a result), and sometimes it is simply desired to allow the coprocess to read from the parent’s STDIN. It is possible to forward the STDIN from the parent process to the coprocess like this:

(
  exec {stdin_fd_copy}<&0

  coproc pass Kubernetes/admin.key <&${stdin_fd_copy}
  cpid="$COPROC_PID"

  kubectl config set-credentials admin \
          --client-key <(cat <&${COPROC[0]}) \
          --client-certificate admin.crt \
          --embed-certs

  wait "$cpid"

  echo $?
)