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.