0

My question is similar to Get exit status of process that's piped to another, but I want to get the exit status of the first command in the pipe within the pipe.

Here's an example:

false | echo "$?"

returns 0 rather than the intended 1.

PIPESTATUS gives the previous error code out:

false | echo "$PIPESTATUS"

returns 0 the first time and then 1, giving the result after the operation itself. My intent with this is to make a function like this:

debug() {
  # Eventually will be formatted as JSON
  echo "$(cat) was the result and the error code was $?"
}

(
  echo "Foo"
  false # To simulate an error
) | debug        # debug should output "Foo was the result and the error code was 1"
0

2 Answers 2

3

You can't do that as such, because the two halves of a pipe execute simultaneously, so the right-side has no way to know whether the left side was successful or not. To do what you want, you have to run the left-side command first, and wait for it to exit. Then you can present the output from the command and the exit code.

You may find that this snippet is more amenable to your purposes:

myfunc() {
  echo foo
  read
}

out="$(myfunc)"
rc=$?
printf '%s was the result and the error code was %d\n' "$out" $rc

$ yes | ./test.sh 
foo was the result and the error code was 0
$ ./test.sh < /dev/null
foo was the result and the error code was 1
2
  • Technically, you could start two processes connected with a pipe at the same time, and then have the second eventually get the exit status of the first through some means. Would be awkward to do in the shell in the least, and the reading side of the pipe would have to buffer all input anyway before proceeding past the point where it needs the exit status to decide what to do. It does rather circumvent the point of a pipeline, though.
    – ilkkachu
    Commented Apr 25, 2023 at 7:45
  • Here, where the eventual output starts with the output of the LHS anyway, one could just run whatever; printf ' was the result and the error code was %d\n' "$?". But if they want to reformat the output as JSON or something, they'll have to buffer. A minor point to note is that command substitution eats trailing newlines, so if you want to avoid that, just using a temporary file might be a thing to consider.
    – ilkkachu
    Commented Apr 25, 2023 at 7:47
0

Jim's Answer is definitely the correct one. However, in my specific use case, I found that the trap command offered me the clean syntax of a pipe while still catching errors.

I ended up going with this:

set -E

trap 'catch $? $LINENO' ERR

catch() {
    echo "{ \"type\": \"error\", \"error\": \"$1\", \"line\": \"$2\" }"
    exit $1
}

debug() {
  # Eventually will be formatted as JSON
  echo "$(cat) was the result"
}

(
  echo "Foo"
  # Simulate an error
  false
  echo "This should not be run!"
) | debug

which outputs:

Foo
{ "type": "error", "error": "1", "line": "18" } was the result

As you can see, echo "This should not be run!" is never executed!

In the end, with the JSON formatted to replace newlines with \n, you get this:

{ "content": "Foo\n{ "error": "1", "line": "18" }" }

It needs a bit of post-processing, specifically to fix the nested quotes and/or to seperate the stdin input from the trap output.

1
  • Thanks for posting this. I don't know what causes the second error, and I'm not sure whether it affects your use case, but notice that without the | debug at the end, your script generates two errors, at line 18 and line 20. With the | debug added, only the first error is reported.
    – Jim L.
    Commented Apr 26, 2023 at 14:44

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .