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

4

37

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.

Obla answered 9/3, 2010 at 22:41 Comment(2)
Why do you not want to use PIPESTATUS?Trihydric
Duplicates: https://mcmap.net/q/426744/-39-tee-39-and-exit-status, #1222333Harv
C
30

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

command 2>&1 | tee -a file.txt ; ( exit ${PIPESTATUS} )
Cryptoclastic answered 9/3, 2010 at 23:6 Comment(0)
F
6

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
Figueroa answered 10/3, 2010 at 3:35 Comment(0)
H
4

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.

Harv answered 10/3, 2010 at 23:25 Comment(0)
G
4

Use process substitution:

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

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

Giorgione answered 2/6, 2015 at 8:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.