How to pipe the stdout of a command, depending on the result of the exit code

Commands in a pipeline run concurrently, that's the whole point of pipes, and inter-process communication mechanism.

In:

cmd1 | cmd2

cmd1 and cmd2 are started at the same time, cmd2 processes the data that cmd1 writes as it comes.

If you wanted cmd2 to be started only if cmd1 had failed, you'd have to start cmd2 after cmd1 has finished and reported its exit status, so you couldn't use a pipe, you'd have to use a temporary file that holds all the data the cmd1 has produced:

 cmd1 > file || cmd2 < file; rm -f file

Or store in memory like in your example but that has a number of other issues (like $(...) removing all trailing newline characters, and most shells can't cope with NUL bytes in there, not to mention the scaling issues for large outputs).

On Linux and with shells like zsh or bash that store here-documents and here-strings in temporary files, you could do:

{ cmd1 > /dev/fd/3 || cmd2 <&3 3<&-; } 3<<< ignored

To let the shell deal with the temp file creation and clean-up.

bash version 5 now removes write permissions to the temp file after creating it, so the above wouldn't work, you'll need to work around it by restoring the write permission first:

{ chmod u+w /dev/fd/3
  cmd1 > /dev/fd/3 || cmd2 <&3 3<&-; } 3<<< ignored

Manually, POSIXly:

tmpfile=$(
  echo 'mkstemp(template)' |
    m4 -D template="${TMPDIR:-/tmp}/XXXXXX"
) && [ -n "$tmpfile" ] && (
  rm -f -- "$tmpfile" || exit
  cmd1 >&3 3>&- 4<&- ||
    cmd2 <&4 4<&- 3>&-) 3> "$tmpfile" 4< "$tmpfile"

Some systems have a non-standard mktemp command (though with an interface that varies between systems) that makes the tempfile creation a bit easier (tmpfile=$(mktemp) should be enough with most implementation, though some would not create the file so you may need to adjust the umask). The [ -n "$tmpfile" ] should not be necessary with compliant m4 implementations, but GNU m4 at least is not compliant in that it doesn't return a non-zero exit status when the mkstemp() call fails.

Also note that there's nothing stopping you running any code in the console. Your "script" can be entered just the same at the prompt of an interactive shell (except for the return part that assumes the code is in a function), though you can simplify it to:

output=$(cmd) || grep foo <<< "$output"

Note: Since the question is tagged bash, I assume you can use bash features.

Given your example code, it seems what you want to do is:

  • Use the command's exit status if it's 0,
  • Use grep's exit status otherwise.

Running grep on an empty pipeline doesn't cost you anything, so one option would be to pipe it anyway, and check the command's exit status, which you can get using the PIPESTATUS array in bash:

$ (echo foo; exit 1) | grep foo
foo
$ echo "${PIPESTATUS[@]}"
1 0  

Here, the subshell exited with 1, grep exited with 0.

So you could do something like:

(command | grep -P "foo"; exit "$((PIPESTATUS[0] ? $? : 0))")

The expression could be simplified, but you get the idea.


There's also the option to simply fake output:

(command && echo foo) | grep -P foo

Where echo foo will run only if the command succeeded, making the grep succeed too.