10

Consider Source code:

1. Parent.sh

#!/usr/bin/ksh
# No tee
ksh Child.sh;
exit_status=$?;
echo "Exit status: ${exit_status}"
# Using tee
ksh Child.sh | tee -a log.txt;
exit_status=$?;
echo "Exit status: ${exit_status}"

2. Child.sh

#!/usr/bin/ksh
...
exit 1;

Output:

Exit status: 1
Exit status: 0

  • Variable $exit_status is capturing the exit status of Child.sh and so is 1.
  • In the 2nd case, $exit_status is capturing the exit status of tee, which is 0.

So how do I capture the exit status and also use tee?

Kent Pawar
  • 1,276
  • 4
  • 16
  • 37

2 Answers2

18

Reproduced (and improved) from the comp.unix.shell FAQ (since I happen to have written that section of the FAQ):

How do I get the exit code of cmd1 in cmd1|cmd2

First, note that cmd1 exit code could be non-zero and still don't mean an error. This happens for instance in

cmd | head -n 1

you might observe a 141 (or 269 with ksh93, or 397 with yash) exit status of cmd, but it's because cmd was interrupted by a SIGPIPE signal when head -n 1 terminated after having read one line.

To know the exit status of the elements of a pipeline

cmd1 | cmd2 | cmd3

with zsh (and fish 3.1+):

The exit codes are provided in the pipestatus special array. cmd1 exit code is in $pipestatus[1], cmd3 exit code in $pipestatus[3], so that $status/$? is always the same as $pipestatus[-1].

with bash:

The exit codes are provided in the PIPESTATUS special array. cmd1 exit code is in ${PIPESTATUS[0]}, cmd3 exit code in ${PIPESTATUS[2]}, so that $? is always the same as ${PIPESTATUS[-1]} (or ${PIPESTATUS[@]: -1} for versions older than 4.2).

with any other Bourne like shells

You need to use a trick to pass the exit codes to the main shell. You can do it using a pipe(2). Instead of running cmd1, you run cmd1; echo "$?" and make sure $? makes its way to the shell.

exec 3>&1
code=`
  # now, inside the backticks, fd4 goes to the pipe
  # whose other end is read and stored in $code  for
  # later evaluation; fd1 is the normal standard output
  # preserved the line before with exec 3>&1

  exec 4>&1 >&3 3>&- 
  {
    cmd1 4>&-; echo "ec1=$?;" >&4
  } | {
    cmd2 4>&-; echo "ec2=$?;" >&4
  } | cmd3 4>&-
  echo "ec3=$?;" >&4
`
exec 3>&-
eval "$code"

Exit codes in $ec1, $ec2, $ec3.

with a POSIX shell

You can use this function to make it easier:

run() {
  j=1
  while eval "\${pipestatus_$j+:} false"; do
    unset "pipestatus_$j"
    j=$(($j+1))
  done
  j=1 com= k=1 l=
  for arg do
    case $arg in
      ('|')
        com="$com {
               $l "'3>&-
               echo "pipestatus_'$j'=$?" >&3
             } 4>&- |'
        j=$(($j+1)) l=;;
      (*)
        l="$l \"\${$k}\""
    esac
    k=$(($k+1))
  done
  com="$com $l"' 3>&- >&4 4>&-
       echo "pipestatus_'$j'=$?"'

  { eval "$(exec 3>&1; eval "$com")"; } 4>&1
  j=1
  ret=0
  while eval "\${pipestatus_$j+:} false"; do
    eval '[ "$pipestatus_'"$j"'" -eq 0 ] || ret=$pipestatus_'"$j"
    j=$(($j+1))
  done
  return "$ret"
}

Use it as:

run cmd1 \| cmd2 \| cmd3

exit codes are in $pipestatus_1, $pipestatus_2, $pipestatus_3 and $? is the right-most non-zero exit status (like with the pipefail option of some shells).

-1

You can use && and || to do it in bash - I assume something similar will work in ksh.

ksh Child.sh && exit_status="$?" || exit_status="$?" | tee -a log.txt;

EDIT:

As @Stephane points out, A && B | C will output A to stdout, and only pipe B to C. It needs to group the outputs, to pipe them together. You can't pass a variable from a subshell into the caller, but, you can do this:

x=$(tempfile) && exit_status=$(ksh Child.sh > $x; echo $?) && (cat $x; rm $x) | tee -a log.txt
Benubird
  • 5,912