Search code examples
bashprocess-substitution

Get exit code of process substitution with pipe into while loop


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?


Solution

  • 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