I'm writing a script in which I want all output (stdout & stderr) to be displayed in my terminal as well as redirected into a log file. Here is the script:
#!/bin/bash
exec &> >(tee -a main.log)
case $1 in
"start")
echo "Starting some services..."
;;
"status")
echo "inactive"
;;
*)
echo "Usage: $0 start | status" 1>&2
exit 1
;;
esac
Now here is an example session of me testing the script:
$ ./main.sh status
$ inactive
$ sudo ./main.sh status
$ cat main.log
inactive
inactive
We can notice:
root
, no output is displayed in my terminal.
inactive
twice in the log file.Can someone explain to me what's happening here? I would want the script to behave the same whether it's run as root
or non-root user.
What's even more surprising to me is when I use the script with bash -x
:
$ bash -x ./main.sh status
+ exec
++ tee -a main.log
$ + case $1 in
+ echo inactive
inactive
$ sudo bash -x ./main.sh status
+ exec
++ tee -a main.log
When run as root
, the script just stops after tee
.
EDIT: I noticed that when run with bash -x
, the rest of the -x
output is redirected to the file. So the script doesn't just stops after tee
.
EDIT: Here's my bash
version
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
wait
ing for a process expansionSince you're using bash 5.x, you can collect the PID of a process expansion and wait
for it.
#!/bin/bash
exec {orig_stdout}>&1 {orig_stderr}>&2
exec > >(tee -a main.log) 2>&1
log_pid=$!
cleanup() {
if [[ $log_pid ]]; then # so we only run cleanup once; this lets it be explicitly called
# restore our original stdout and stderr; closes the input to tee
exec >&$orig_stdout 2>&$orig_stderr
# since tee's input has closed, it will exit; wait for that to happen
wait "$log_pid"
# clear the variable for that PID so if we run cleanup again it's a noop
unset log_pid
fi
}
trap cleanup EXIT
# ...rest of your script goes here
cleanup # in theory unnecessary because of the trap, but legal.
This is what the preexisting answer does, but in a manner more cautious to be correct and secure
#!/bin/bash
if [[ ! $forked ]]; then
forked=1 exec -a "$0" bash "${BASH_SOURCE[0]}" "$@" | tee -a main.log
exit
fi
# ...rest of your script goes here
(${BASH_SOURCE[0]}
is more reliable than $0
as described in BashFAQ #28).