5
votes

in bash, if I execute a couple of commands piped together inside of backticks, how can I find out the exit status of the first command?

i.e. in this case, I am trying to get the "1". which I can get via PIPESTATUS[0] if I am not using backticks, but which doesn't seem to work when I want to saving the output:

## PIPESTATUS[0] works to give me the exit status of 'false':
$ false | true;
$ echo $? ${PIPESTATUS[0]} ${PIPESTATUS[1]};
0 1 0

## doesn't work:
$ a=`false | true`;
$ echo $? ${PIPESTATUS[0]} ${PIPESTATUS[1]};
0 0

More generally, I am trying to accomplish: save the last line of the output of some program to a variable, but be able to tell if the program failed:

$ myvar=` ./someprogram | tail -1 `;
$ if [ "what do i put here" ]; then echo "program failed!"; fi

Ideally I'd also like to understand what is going on, not just what the answer is.

Thanks.

3

3 Answers

4
votes

Try to set pipefail option. It returns the last command of the pipeline that failed. One example:

First I disable it:

set +o pipefail

Create a perl script (script.pl) to test the pipeline:

#!/usr/bin/env perl

use warnings;
use strict;

if ( @ARGV ) { 
    die "Line1\nLine2\nLine3\n";
}
else {
    print "Line1\nLine2\nLine3\n";
}

Run in command-line:

myvar=`perl script.pl | tail -1`
echo $? "$myvar"

That yields:

0 Line3

It seems correct, let see with pipefail enabled:

set -o pipefail

And run the command:

myvar=`perl script.pl fail 2>&1 | tail -1`
echo $? "$myvar"

That yields:

255 Line3
2
votes

My solution was using fifos and the bash "coproc" builtin to get the messages and status from each command in the pipe. I'd never used fifos before. (oh boy, next time I'm using BashEclipse on Fedora). It turned into a generalized mechanism for managing any pipe command. I solved the problem, but not in 10 or 20 lines of code. more like 200 for a robust drop-in re-usable solution (took me three days to do so).

I share my notes:

* stderr for all pipe commands goes to the fifos.  
  If you want to get messages from stdout, you must redirect '1>&2', like this:  
  PIPE_ARRAY=("cat ${IMG}.md5" "cut -f1 -d\" \" 1>&2")  
  You must put "2>/fifo" first. Otherwise it won\'t work. example:  
    cat ${IMG}.md5 | cut -f1 -d' ' 1>&2  
  becomes:  
    cat ${IMG}.md5 2>/tmp/fifo_s0 | cut -f1 -d" " 2>/tmp/fifo_s1 1>&2 ; PSA=( "${PIPESTATUS[@]}" )

* With more tha one fifo, I found that you must read each fifo in turn.  
  When "fifo1" gets written to, "fifo0" reads are blocked until you read "fifo1"  
  I did\'nt use any special tricks like "sleep", "cat", or extra file descriptors  
  to keep the fifos open.  

* PIPESTATUS[@] must be copied to an array immediately after the pipe command returns.  
  _Any_ reads of PIPESTATUS[@] will erase the contents. Super volatile !  
  "manage_pipe()" appends '; PSA=( "${PIPESTATUS[@]}" )' to the pipe command string  
  for this reason. "$?" is the same as the last element of "${PIPESTATUS[@]}",  
  and reading it seems to destroy "${PIPESTATUS[@]}", but it's not absolutly verifed.

run_pipe_cmd() {  
  declare -a PIPE_ARRAY MSGS  
  PIPE_ARRAY=("dd if=${gDEVICE} bs=512 count=63" "md5sum -b >${gBASENAME}.md5")  
  manage_pipe PIPE_ARRAY[@] "MSGS"  # (pass MSGS name, not the array) 
}  
manage_pipe () {
  # input  - $1 pipe cmds array, $2 msg retvar name
  # output - fifo msg retvar
  # create fifos, fifo name array, build cnd string from $1 (re-order redirection if needed)
  # run coprocess 'coproc execute_pipe FIFO[@] "$CMDSTR"'
  # call 'read_fifos FIFO[@] "M" "S"' (pass names, not arrays for $2 and $3)
  # calc last_error, call _error, _errorf
  # set msg retvar values (eval ${2}[${i}]='"${Msg[${i}]}"')
}
read_fifos() {  
  # input  - $1 fifo array, $2 msg retvar name, $3 status retvar name  
  # output - msg, status retvars  
  # init local fifo_name, pipe_cmd_status, msg arrays  
  # do read loop until all 'quit' msgs are received
  # set msg, status retvar values (i.e. eval ${3}[${i}]='"${Status[${i}]}"' 
}
execute_pipe() {  
  # $1 fifo array, $2 cmdstr, $3 msg retvar, $4 status retvar   
  # init local fifo_name, pipe_cmd_status arrays
  # execute command string, get pipestaus  (eval "$_CMDSTR" 1>&2)
  # set fifo statuses from copy of PIPESTATUS
  # write 'status', 'quit' msgs to fifo  
}
0
votes

The problem is that backticks launch a sub-shell. Your sub-shell has its own ${PIPESTATUS[@]} array, but that does not persist into the parent shell. Here's a trick to shove it into the output variable $a and then retrieve it into a new array called ${PIPESTATUS2[@]}:

## PIPESTATUS[0] works to give me the exit status of 'false':
$ false | true
$ echo $? ${PIPESTATUS[0]} ${PIPESTATUS[1]}
0 1 0

## Populate a $PIPESTATUS2 array:
$ a=`false | true; printf :%s "${PIPESTATUS[*]}"`
$ ANS=$?; PIPESTATUS2=(${a##*:})
$ [ -n "${a%:*}" ] && a="${a%:*}" && a="${a%$'\n'}" || a=""
$ echo $ANS ${PIPESTATUS2[0]} ${PIPESTATUS2[1]};
0 1 0

This saves the sub-shell's ${PIPESTATUS[@]} array as a space-delimited list of values at the end of $a and then extracts it using shell variable substring removal (see the longer example and description I gave to this similar question). The third line is only needed if you actually want to save the value of $a without the extra statuses (as if it were run as false | true in this example).