bash: redirect (and append) stdout and stderr to file and terminal and get proper exit status

To redirect (and append) stdout and stderr to a file, while also displaying it on the terminal, I do this:

command 2>&1 | tee -a file.txt

However, is there another way to do this such that I get an accurate value for the exit status?

That is, if I test $?, I want to see the exit status of command, not the exit status of tee.

I know that I can use ${PIPESTATUS[0]} here instead of $?, but I am looking for another solution that would not involve having to check PIPESTATUS.

Answers


Perhaps you could put the exit value from PIPESTATUS into $?

command 2>&1 | tee -a file.txt ; ( exit ${PIPESTATUS} )

Another possibility, with some bash flavours, is to turn on the pipefail option:

pipefail

If set, the return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status, or zero if all commands in the pipeline exit successfully. This option is disabled by default.

set -o pipefail
...
command 2>&1 | tee -a file.txt || echo "Command (or tee?) failed with status $?"

This having been said, the only way of achieving PIPESTATUS functionality portably (e.g. so it'd also work with POSIX sh) is a bit convoluted, i.e. it requires a temp file to propagate a pipe exit status back to the parent shell process:

{ command 2>&1 ; echo $? >"/tmp/~pipestatus.$$" ; } | tee -a file.txt
if [ "`cat \"/tmp/~pipestatus.$$\"`" -ne 0 ] ; then
  ...
fi

or, encapsulating for reuse:

log2file() {
  LOGFILE="$1" ; shift
  { "$@" 2>&1 ; echo $? >"/tmp/~pipestatus.$$" ; } | tee -a "$LOGFILE"
  MYPIPESTATUS="`cat \"/tmp/~pipestatus.$$\"`"
  rm -f "/tmp/~pipestatus.$$"
  return $MYPIPESTATUS
}

log2file file.txt command param1 "param 2" || echo "Command failed with status $?"

or, more generically perhaps:

save_pipe_status() {
  STATUS_ID="$1" ; shift
  "$@"
  echo $? >"/tmp/~pipestatus.$$.$STATUS_ID"
}

get_pipe_status() {
  STATUS_ID="$1" ; shift
  return `cat "/tmp/~pipestatus.$$.$STATUS_ID"`
}

save_pipe_status my_command_id ./command param1 "param 2" | tee -a file.txt
get_pipe_status my_command_id || echo "Command failed with status $?"

...

rm -f "/tmp/~pipestatus.$$."* # do this in a trap handler, too, to be really clean

Use process substitution:

command > >( tee -a "$logfile" ) 2>&1

tee runs in a subshell so $? holds the exit status of command.


There is an arcane POSIX way of doing this:

exec 4>&1; R=$({ { command1; echo $? >&3 ; } | { command2 >&4; } } 3>&1); exec 4>&-

It will set the variable R to the return value of command1, and pipe output of command1 to command2, whose output is redirected to the output of parent shell.


Need Your Help

Insert multiple rows where not exists PostgresQL

postgresql postgresql-9.2

I'd like to generate a single sql query to mass-insert a series of rows that don't exist on a table. My current setup makes a new query for each record insertion similar to the solution detailed in...