1

In bash, this:

exec &> >(tee -a "$LOG_FILE")

Will redirect stderr & stdout to tee that allows the output to be displayed on the terminal while also logged (written to $LOG_FILE), but it uses process substitution, a feature not available in sh

If I run the script with sh:

sh script

I get:

syntax error near unexpected token `>'

In my attempt to have the same result in sh, I tried:

exec 2>&1 | tee -a "$LOG_FILE"

But I notice that nothing gets written to $LOG_FILE, instead I had to use:

exec > "$LOG_FILE" 2>&1

That writes the log to the file, but I don't see any output when running the script.

Any ideas?

From the accepted answer, the usage of:

mkfifo "$HOME/.pipe.$$"

helps/complements the suggested answer, since allows to have the code inline and not within a block

nbari
  • 616
  • what is sh though? in my system it's a symlink to dash, what is it in yours? – Jaromanda X May 09 '23 at 07:15
  • is /usr/bin/sh – nbari May 09 '23 at 07:22
  • @nbari, Jaromanda is asking about the output of ls -l /usr/bin/sh. For me it is: /usr/bin/sh -> dash. – Stewart May 09 '23 at 07:30
  • 1
    that error message (with the backtick as the opening quote) looks like one from an old version of Bash, one that doesn't support process substitution. You should probably [edit] to mention your system. Also, the Bourne shell is a historical ancestor of the current (POSIX) sh, you probably don't have that. (Unless you're running something like Solaris that might be stuck in the past a bit.) – ilkkachu May 09 '23 at 07:46
  • /usr/bin/sh is usually a symlink, what is it linked to – Jaromanda X May 09 '23 at 08:05
  • Interesting that I see that sh is linked to bash: /usr/bin/sh -> bash (testing on a red-hat 8) – nbari May 10 '23 at 09:20

2 Answers2

1

Here you go

#!/bin/sh
#
LOG_FILE=/tmp/log_file    # Demonstration target

Set up the redirection to tee

mkfifo "$HOME/.pipe.$$" tee -a "$LOG_FILE" <"$HOME/.pipe.$$" & exec 1>"$HOME/.pipe.$$" 2>&1 rm -f "$HOME/.pipe.$$"

Using the redirection

echo hello date echo STDERR >&2

exit 0

The guts of the conversion revolves around creating the pipe, redirecting stdin for the tee, and redirecting stdout and stderr for the remainder of the process. In the middle of that is the invocation of tee itself:

tee -a "$LOG_FILE" <"$HOME/.pipe.$$" &

Here we're writing (appending) to "$LOG_FILE" by reading from the pipe we created just a moment earlier, and the trailing & runs the command in the background - as usual. Once the pipe has been opened on both sides (writer and reader) it can be deleted from the filesystem: it still exists and can be used until both sides are finished with it, but it is no longer accessible by name.

In an earlier edit of this script I captured the PID of the tee command into the variable $tpid, ready for killing it at the end of the script. However, it was correctly pointed out to me that I don't have to kill the tee process at the end of the script because it'll get a fatal SIGPIPE anyway once the writer finally closes the pipe.

In production code I assume you'll have $LOG_FILE writing somewhere safe. I've dropped it into /tmp simply as a placeholder for the remainder of the code to function.

Chris Davies
  • 116,213
  • 16
  • 160
  • 287
  • 1
    Creating (or writing to) files with fixed names in world-writable areas such as /tmp is bad practice from a security standpoint, I would not advertise such practice. – Stéphane Chazelas May 09 '23 at 08:51
  • @StéphaneChazelas the guts of the code doesn't write to /tmp for exactly the same reasons as you're highlighting. I don't care about $LOG_FILE but it was a given in the question so I left it as capitals and simply pinned it somewhere – Chris Davies May 09 '23 at 09:10
  • tee -a "$LOG_FILE" writes to that file. If someone has made a ln -s /bin/sh /tmp/log_file and you're running this as root for instance, that will wreak havoc. – Stéphane Chazelas May 09 '23 at 09:13
  • @StéphaneChazelas LOG_FILE isn't my problem and not relevant to the code being demonstrated – Chris Davies May 09 '23 at 09:13
  • The problem is with your LOG_FILE=/tmp/log_file line which should be LOG_FILE=/var/log/file.log or LOG_FILE=~/.local/file.log, for instance. – Stéphane Chazelas May 09 '23 at 09:15
1

Just do:

#! /bin/sh -
LOG_FILE=/var/log/myscript.log
{
  the whole script here:
  ...
} 2>&1 | tee -a -- "$LOGFILE"

Or:

#! /bin/sh -
LOG_FILE=/var/log/myscript.log
main() {
  the whole script here:
  ...
}
main "$@" 2>&1 | tee -a -- "$LOGFILE"

Beware the script exit status will be tee's though. That can be worked around with the pipefail option, but though that option will be standard in sh in the next version of the standard, there are still some sh implementations that don't support it (most notably dash). See How do I capture the return status and use tee at the same time in korn shell? for other approaches.

Note that while sh was generally the Bourne shell in the 80s (succeeding to the Thompson shell), nowadays, sh is one or the other implementation of an interpreter for the standard POSIX sh language which is based on a subset of that of ksh88 (a Bourne shell derivative), not the Bourne shell. Process substitution is from ksh (ksh86 initially), but has not been specified for sh by POSIX and is not available in shells derived from the Almquist shell (initially a clone of the Bourne shell) or the Forsyth shell (including pdksh/mksh even though they were initially meant to be clones of the Korn shell). It's also not available in ksh88 on systems that don't support /dev/fd/n.