I did some further testing of the code that was suggested. On reflection it wasn't complete as per my original question. For that reason, I'm posting here the working code, which has been called a few times to prove it works in all possible modes (with basic arguments only - not a list as has been suggested will not work)
I found it fascinating that the redirect to tee needs a sleep 0 afterwards because the first command to echo will often do that prior to the tee being set up. I don't know how to wait only until that redirect is set before continuing.
The following code echos quite a bit to the various destinations, each echo is done via a method to ensure a unique sequential id. This was needed to confirm they are logged in the correct order.
The initial solution unfortunately has the bug my question has, that is log statements would be out of order. This is less neat because I need to bookend the block I want redirected to script_output.log with two statements rather than just wrap it in {}
, but at least it keeps things as you'd expect from execution order.
#!/bin/bash
script_log="script_output.log"
process_log="output.log"
function store_output_redirects() {
exec 3>&1 4>&2
}
function redirect_all_output_to_file() {
exec 1>> "${script_log}" 2>&1
}
function clear_output_redirects() {
exec 1>&3 2>&4
}
#!/bin/bash
my_function() {
local pid_file=$1; logfile=$2
local tee_output="none"
if [ "$3" = "tee" ] || [ "$3" = "tee_console" ] ; then
tee_output=$3
shift
fi
cmd=("${@:3}")
log "console: inside function"
store_output_redirects
redirect_all_output_to_file
{
# this block is redirected to ${script_log}
log "${script_log}: inside redirected block"
if [[ "$logfile" = "console" ]]; then
# swap the output to the default (console)
clear_output_redirects
elif [ "$tee_output" = "tee" ] ; then
#clear_output_redirects
exec 1> >(tee "$logfile")
# insert a tiny time delay for the tee redirect to be set up (otherwise output will go to the previous destination).
sleep 0
elif [ "$tee_output" = "tee_console" ] ; then
clear_output_redirects
exec 1> >(tee "$logfile")
# insert a tiny time delay for the tee redirect to be set up (otherwise output will go to the previous destination).
sleep 0
else
exec >>"$logfile"
# insert a tiny time delay for the tee redirect to be set up (otherwise output will go to the previous destination).
sleep 0
fi
echo "$BASHPID" > "$pid_file"
if ! [ "$tee_output" = "none" ] ; then
log "tee: command to be executed with output to $logfile and console: ${cmd[*]}"
else
log "$logfile: command to be executed with output to $logfile: ${cmd[*]}"
fi
# this cannot be escaped as per shellcheck.net as error 'command not found' when not using tee.
${cmd[@]}
# reset the defaults for this block
redirect_all_output_to_file
log "${script_log}: end of redirected block"
}
clear_output_redirects
log "console: function ends"
}
function log() {
(( log_line = log_line + 1 ))
echo "${log_line} $*"
}
function print_log() {
local logfile="$1"
echo
if ! [ -f "$logfile" ] ; then
echo "log file $logfile does not exist"
else
echo Contents of "$logfile":
cat "$logfile" | while read -r logline
do
echo "$logline"
done
fi
}
function test_redirection() {
log_line=0
local cmd_log_file=$1; shift
local tee=$1; shift
[ -f "${process_log}" ] && rm "${process_log}"
[ -f "${script_log}" ] && rm "${script_log}"
log "console: Redirecting requested cmd output to $cmd_log_file"
log console: making copy of the stdout and stderr destinations
log console: calling function
my_function cmd.pid "$cmd_log_file" "$tee" echo hi
log console: stdout and sterr reset to the defaults for the next test
print_log "$script_log"
echo
print_log "$process_log"
}
echo "*****************************"
echo "** Testing output to console"
echo "*****************************"
test_redirection "console"
echo "*****************************"
echo "** Testing output to logfile"
echo "*****************************"
test_redirection "$process_log"
echo "*****************************"
echo "** Testing output to logfile and console"
echo "*****************************"
test_redirection "$process_log" "tee_console"
echo "*****************************"
echo "** Testing output to logfile and default"
echo "*****************************"
test_redirection "$process_log" "tee"