7

Expanding from this question, we have a use case where we want to pipe the stdout of a command depending on whether that command succeeded or failed.

We start with a basic pipe

command | grep -P "foo"

However we notice that sometimes command does not output anything to stdout, but does have an exit code of 0. We want to ignore this case and only apply the grep when the exit code is 1

For a working example, we could implement a command like this:

OUTPUT=$(command) # exit code is 0 or 1
RESULT=$?
if [ $RESULT -eq 0 ]; then
  return $RESULT; # if the exit code is 0 then we simply pass it forward
else
  grep -P "foo" <<< $OUTPUT; # otherwise check if the stdout contains "foo"
fi

but this has a number of disadvantages, namely that you have to write a script, meaning you can't just execute it in the console. It also seems somewhat amateurish.

For a more concise syntax, I'm imagining a fictional ternary operator which pipes if the exit code is 1, otherwise it passes the exit code forwards.

command |?1 grep -P "foo" : $?

Is there a series of operators and utils that will achieve this result?

2 Answers2

18

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"
  • 1
    Excellent. Both cmd1 > file || cmd2 < file and output=$(cmd) || grep foo <<< "$output" achieve the results in full. With the former syntax I have to later clean up by removing the file, but this is not a big deal and provides quite a few options. Thanks. – I'll Eat My Hat Oct 25 '18 at 19:37
  • 2
    See also chronic from moreutils which addresses a similar situation – derobert Oct 25 '18 at 19:49
  • 2
    use (f=\mktemp`; exec 3>$f 4<$f; rm $f; cmd1 >&3 || cmd2 <&4)` if you don't want to have to clean up afterwards. –  Oct 26 '18 at 00:46
5

Note: Since the question is tagged , 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.

muru
  • 72,889