Search code examples
bashexit-code

Why does a Bash while loop result in PIPESTATUS "1" instead of "0"?


GNU bash, version 5.2.15(1)-release (x86_64-pc-linux-gnu)

Consider this simple while loop:

while false; do true; done; echo "${PIPESTATUS[0]}"

It prints 1 instead of 0. This is unexpected to me and I can't find any documentation about this.

The same loop, when checked for actual exit code:

while false; do true; done; echo $?

prints 0, as expected.

I also noticed that if a break is used in the body:

while true; do true; break; done; echo "${PIPESTATUS[0]}"

then both the PIPESTATUS as well as the exit code are both 0, as expected.

Setting pipefail option does not seem to change this behaviour.

My guess: I guess the loop condition is technically the last statement to be executed, and hence its exit code is returned in PIPESTATUS. This explains why break did not return 1 while false loop condition does. However, this leads to more questions:

  • Why the disparity between PIPESTATUS and actual exit code?
  • Won't all while loops end with PIPESTATUS 1, since every loop has to stop due loop condition being false? (Except the loops that end due to a break)
  • Does this mean it is a reliable way to tell whether the loop exited to loop condition becoming false or because of a break?

Thanks in advance for your help.


Solution

  • From man bash:

    man bash | grep -A3 PIPESTATUS
      PIPESTATUS
             An  array  variable (see Arrays below) containing a list of exit
             status values from the processes in  the  most-recently-executed
             foreground pipeline (which may contain only a single command).
    
    man bash | sed -e '/^ *? /{N;q};d'
      ?      Expands  to  the exit status of the most recently executed fore‐
             ground pipeline.
    

    So,

    • $PIPESTATUS holds the result of the last forked command (false in this case),
    • $? contains the status of the last script command (while ...;do ...;done).

    Then

    if ! :; then :;fi ; echo ${?@Q} ${PIPESTATUS[@]@Q}
    '0' '0'
    
    while ! :; do :;done ; echo ${?@Q} ${PIPESTATUS[@]@Q}
    '0' '0'
    
    if /bin/false; then :;fi ; echo ${?@Q} ${PIPESTATUS[@]@Q}
    '0' '1'
    
    while false; do :;done ; echo ${?@Q} ${PIPESTATUS[@]@Q}
    '0' '1'
    

    More at this difference of $? and ${PIPESTATUS[0]} @ lists.gnu.org.

    $? is POSIX, while PIPESTATUS is a array...

    if ls /wrong/path | wc | sed 'w/wrong/path' >/dev/null ; then
        echo Don't print this'
    fi ; echo ${?@Q} ${PIPESTATUS[@]@A}
    
    ls: cannot access '/wrong/path': No such file or directory
    sed: couldn't open file /wrong/path: No such file or directory
    '0' declare -a PIPESTATUS=([0]="2" [1]="0" [2]="4")
    

    Where $PIPESTATUS hold result of all 3 commands ls, wc and sed.

    if ls /wrong/path | wc | cat - /wrong/path | sed 'w/wrong/path' >/dev/null ; then
        echo Don't print this'
    fi ; echo ${?@Q} ${PIPESTATUS[@]@A}  $(( $? ${PIPESTATUS[@]/#/+} ))
    
    ls: cannot access '/wrong/path': No such file or directory
    cat: /wrong/path: No such file or directory
    sed: couldn't open file /wrong/path: No such file or directory
    '0' declare -a PIPESTATUS=([0]="2" [1]="0" [2]="1" [3]="4") 7
    
    • The group command if ... then ...(elif ... then ) ... (else ... then) ... fi was executed successfully. His result code is 0, but

    • ls's result code was 2,

    • wc's result code was 0,

    • cat's result code was 1 and

    • sed's result code was 4.

      until ls -ld / | wc | cat - | sed -n '/[a-z]/p' ; do echo Don't print this' done ; echo ${?@Q} ${PIPESTATUS[@]@A} $(( $? ${PIPESTATUS[@]/#/+} ))

    '0' declare -a PIPESTATUS=([0]="0" [1]="0" [2]="0" [3]="0") 0
    

    or with some cosmetic:

    until ls -ld / | wc | cat - | sed -n '/[a-z]/p' ; do
        echo Don't print this'
    done; printf 'RC: $?=%s PIPESTATUS=(%s) Total=%d\n' \
        $? "${PIPESTATUS[*]}"  $(( $? ${PIPESTATUS[*]/#/+} ))
    
    RC: $?=0 PIPESTATUS=(0 0 0 0) Total=0
    
    while ls -ld /wrong/path | wc | cat /tmp/noperm - | sed -n '/[a-z]/w/wrong/path' ; do
        echo Don't print this'
    done ;printf 'RC: $?=%s PIPESTATUS=(%s) Total=%d\n' \
        $? "${PIPESTATUS[*]}"  $(( $? ${PIPESTATUS[*]/#/+} ))
    
    ls: cannot access '/wrong/path': No such file or directory
    cat: /tmp/noperm: Permission denied
    sed: couldn't open file /wrong/path: No such file or directory
    RC: $?=0 PIPESTATUS=(2 0 141 4) Total=147
    

    ... But if not is a group command:

    ls -ld /wrong/path | wc | cat /tmp/noperm - | 
      sed -n '/[a-z]/w/wrong/path'; printf 'RC: $?=%s PIPESTATUS=(%s) Total=%d\n' \
        $? "${PIPESTATUS[*]}"  $(( $? ${PIPESTATUS[*]/#/+} ))
    
    ls: cannot access '/wrong/path': No such file or directory
    sed: couldn't open file /wrong/path: No such file or directory
    cat: /tmp/noperm: Permission denied
    RC: $?=4 PIPESTATUS=(2 0 141 4) Total=151