17

The following script calls another program reading its output in a while loop (see Bash - How to pipe input to while loop and preserve variables after loop ends):

while read -r col0 col1; do
    # [...]
done < <(other_program [args ...])

How can I check for the exit code of other_program to see if the loop was executed properly?

zx8754
  • 52,746
  • 12
  • 114
  • 209
Philipp H.
  • 1,513
  • 3
  • 17
  • 31

4 Answers4

12

Note: ls -d / /nosuch is used as an example command below, because it fails (exit code 1) while still producing stdout output (/) (in addition to stderr output).

Bash v4.2+ solution:

ccarton's helpful answer works well in principle, but by default the while loop runs in a subshell, which means that any variables created or modified in the loop will not be visible to the current shell.

In Bash v4.2+, you can change this by turning the lastpipe option on, which makes the last segment of a pipeline run in the current shell;
as in ccarton's answer, the pipefail option must be set to have $? reflect the exit code of the first failing command in the pipeline:

shopt -s lastpipe  # run the last segment of a pipeline in the current shell
shopt -so pipefail # reflect a pipeline's first failing command's exit code in $?

ls -d / /nosuch | while read -r line; do 
  result=$line
done

echo "result: [$result]; exit code: $?"

The above yields (stderr output omitted):

result: [/]; exit code: 1

As you can see, the $result variable, set in the while loop, is available, and the ls command's (nonzero) exit code is reflected in $?.


Bash v3+ solution:

ikkachu's helpful answer works well and shows advanced techniques, but it is a bit cumbersome.
Here is a simpler alternative:

while read -r line || { ec=$line && break; }; do   # Note the `|| { ...; }` part.
    result=$line
done < <(ls -d / /nosuch; printf $?)               # Note the `; printf $?` part.

echo "result: [$result]; exit code: $ec"
  • By appending the value of $?, the ls command's exit code, to the output without a trailing \n (printf $?), read reads it in the last loop operation, but indicates failure (exit code 1), which would normally exit the loop.

  • We can detect this case with ||, and assign the exit code (that was still read into $line) to variable $ec and exit the loop then.


On the off chance that the command's output doesn't have a trailing \n, more work is needed:

while read -r line || 
  { [[ $line =~ ^(.*)/([0-9]+)$ ]] && ec=${BASH_REMATCH[2]} && line=${BASH_REMATCH[1]};
    [[ -n $line ]]; }
do
    result=$line
done < <(printf 'no trailing newline'; ls /nosuch; printf "/$?")

echo "result: [$result]; exit code: $ec"

The above yields (stderr output omitted):

result: [no trailing newline]; exit code: 1
Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Didn't know about `shopt -s lastpipe` so far. I have been using process substitution as an alternative so far, but today I realized that `set -e` doesn't cause shell to abort when there are errors inside the process substitution, so switched back to the more natural looking pipe approach with `lastpipe` option. – haridsv Feb 05 '18 at 13:39
8

At least one way would be to redirect the output of the background process through a named pipe. This would allow to pick up its PID and then get the exit status through waiting on the PID.

#!/bin/bash
mkfifo pipe || exit 1
(echo foo ; exit 19)  > pipe &
pid=$!
while read x ; do echo "read: $x" ; done < pipe
wait $pid
echo "exit status of bg process: $?"
rm pipe

If you can use a direct pipe (i.e. don't mind the loop being run in a subshell), you could use Bash's PIPESTATUS, which contains the exit codes of all commands in the pipeline:

(echo foo ; exit 19) | while read x ; do 
  echo "read: $x" ; done; 
echo "status: ${PIPESTATUS[0]}" 
mklement0
  • 382,024
  • 64
  • 607
  • 775
ilkkachu
  • 6,221
  • 16
  • 30
3

A simple way is to use the bash pipefail option to propagate the first error code from a pipeline.

set -o pipefail
other_program | while read x; do
        echo "Read: $x"
done || echo "Error: $?"
ccarton
  • 3,556
  • 16
  • 17
  • Nicely done, though it's worth mentioning that the `while` loop then runs in a subshell and its variables won't be visible to the current shell. In Bash v4.2+ you can use `shopt -s lastpipe` to prevent this. – mklement0 May 02 '17 at 12:24
3

Another way is to use coproc (requires 4.0+).

coproc other_program [args ...]
while read -r -u ${COPROC[0]} col0 col1; do
    # [...]
done
wait $COPROC_PID || echo "Error exit status: $?"

coproc frees you from having to setup asynchronicity and stdin/stdout redirection that you'd otherwise need to do in an equivalent mkfifo.

John B
  • 3,566
  • 1
  • 16
  • 20
  • 1
    This works great, but it should be mentioned that one has to save `$COPROC_PID` to some other variable since it will be unset when the command terminates, resulting in `wait` to be called without a PID. See https://unix.stackexchange.com/a/338022 for details. It should also be mentioned that this approach may fail when executing commands that are very fast to finish, e.g. `cat `. In this case, the command finishes before the loop is entered, resulting in `read` trying to read from the no longer existing file descriptor `${COPROC[0]}`. – Fonic Mar 15 '18 at 06:04