0

I'm trying to modify a script that uses the following:

# first portion of script
# ...
exec > >(tee -ia $OUT)
# ...
# second portion of script 

The problem I have with this script is that it produces voluminous output to stdout (my terminal). The script author included no options for eliminating the terminal output. I would like to add an option that removes the stdout, and get the output solely in the file $OUT.

Here's what I've tried:

TERM_OPT="OFF"

first portion of script

...

if [ $TERM_OPT != "OFF" ]; then exec > >(tee -ia $OUT) else {

...

second portion of script

} > $OUT fi

This seems to work, but I'm unsure about the use of curly braces {} in this context as the GNU secion of Grouping Commands (seems to) state that a semicolon ; is required following the list. But adding a ; or leaving it off seems to make no difference. I've wondered if I should use parentheses () instead of curly braces, but this causes everything inside the () to execute in a subshell. I'm not particularly keen on that as it's someone else's script & the subshell implications are unclear to me (I didn't try this).

The other thing I tried seemed like a hack, but I read of others using it, and it seems to work OK also:

TERM_OPT="OFF"

first portion of script

...

if [ $TERM_OPT != "OFF" ]; then exec > >(tee -ia $OUT) else exec > >(tee -ia $OUT 1> /dev/null) fi

...

second portion of script

I like this as it seems more self-contained, but that's not much of a consideration AFAICT.

So the Question is: What's the correct way to do this? By that, I mean what's the correct way to opt out of the terminal output after an exec > >(tee -ia $OUT)? Is one of these solutions preferable to the other - or do I need to do something completely different?

Seamus
  • 2,925

2 Answers2

3

Only redirecting to the $OUT file in append mode is much easier:

case $TERM_OPT in
  (OFF) exec >> "$OUT";;
  (*)   exec > >(tee -ia -- "$OUT")
esac

Beware the shell doesn't wait for tee upon exiting, so you might find that tee is still outputting something after your script has finished.

$ bash -c 'exec > >(tee -ia -- file); echo A'; echo B; sleep 1
B
A

See how A was output after B.

To work around that, you can add:

if [ "$TERM_OPT" != OFF ]; then
  exec > /dev/null # closes the pipe to tee which should cause tee
                   # to see EOF on input and exit
  wait # waits for all asynchronous tasks including tee
fi

At the end of the script or in an EXIT trap.

Another option (which would also work in sh) is to put your whole script inside a main function and do:

#! /bin/sh -
main() {
  # your script here
}
case $TERM_OPT in
  (OFF) main "$@" >> "$OUT";;
  (*)   main "$@" | tee -ia -- "$OUT"
esac
  • What's the significance of the double dash --? – Seamus Jan 08 '24 at 08:29
  • Still a bit fuzzy on this (though your explanation was quite good)... does the -- mark the last option, and therefore anything following must be an argument? – Seamus Jan 08 '24 at 08:49
  • 1
    Yes, it marks the end of options, you use it to make sure nothing past it will be treated as an option even if it starts with - or +. Here, $OUT is variable so may very well start with - like in OUT=---myfile---.log – Stéphane Chazelas Jan 08 '24 at 09:06
  • One more Q: "Beware the shell doesn't wait for tee upon exiting, ..." I would think you would want the shell to wait on tee?? By that I mean, if tee has not finished writing the output file (due to a slow HDD maybe) shouldn't the shell keep the process open until it finishes? – Seamus Jan 08 '24 at 09:29
  • Thing is here, tee doesn't finish until it sees the end of its input. If the shell, whose stdout is redirected to the pipe to tee doesn't terminate then tee doesn't terminate so the shell would never stop if it was waiting for tee. You need to actively close that pipe like we do here with exec > /dev/null for tee to terminate (assuming there's no other background process with stdout or other fd redirected to that pipe). – Stéphane Chazelas Jan 08 '24 at 09:41
2

If you want to redirect stdout to be appended to a file, and not also have a copy of the output on the terminal, just do:

exec >> "$OUT"

The only purpose of tee is to duplicate one stream into multiple, so if you only want the output in one place, there's no need for it. Your tee -ia "$OUT" > /dev/null would of course also work, but that's a bit silly: you'd still be duplicating the output, just to discard the other copy.

I'm unsure about the use of curly braces {} in this context

Braces require a semicolon, or a newline. It actually says that in the manual: "The semicolon (or newline) following list is required." Parenthesis require neither.

The implications of a subshell are that the subshell runs in an independent copy of the shell, and any changes in the subshell are not visible outside it. If the script ends anyway when the subshell ends, that doesn't mean much, and all the subshell does is start an unneeded process.

I would go with something like

if [ "$TERM_OPT" != "OFF" ]; then
     exec > >(tee -ia -- "$OUT")
else
     exec >> "$OUT"
fi
ilkkachu
  • 138,973