Is there a way to use tee with the whole script from INSIDE the script?

I’m making a script to update the system (Ubuntu 22.04), updating all packages in apt, flatpak and snap with one order (like sudo ./update.sh) and, in case something goes wrong, I want to save the output in a file. I have two ways to do this.

One is to redirect the standard output of every order with "tee", like:

echo "### apt update:n" | tee update.out
apt update | tee -a  update.out

# etc, etc all with "| tee -a update.out"

Which is the kind of repetition that you’d want to avoid in programming.

The other way I could do it is using "tee" calling the script itself like:

sudo ./update.sh | tee update.out

But if I want this behavior by default, the smart move is to put this inside the script, right?

How could I do this, so when I do sudo ./update.sh, it sends the whole output to the screen and the output file?

Asked By: Carlos González

||

Note: if you’re using a shell other than Bash or Zsh, you may need to replace (in a more portable fashion) |& with 2>&1 |

You can wrap the whole script in braces and pipe the output to tee.

Bash provides two ways to group a list of commands to be executed as a
unit. When commands are grouped, redirections may be applied to the
entire command list. For example, the output of all the commands in
the list may be redirected to a single stream. […] Placing a list of
commands between curly braces causes the list to be executed in the
current shell context. No subshell is created. The semicolon (or
newline) following list is required.

This first example is to be used only in case you want to log just stdout, and in case errors aren’t of concern; see the second example for a better solution (this example has the adverse / wanted effect of not preserving stderr in the output file, and the adverse effect of not preserving the order of stdout / stderr in the terminal, in general I suggest you just use the second method which will Just Work(tm).

#!/usr/bin/env bash

{
        echo foo
        echo bar
} | tee log.log

exit 0
~ % ./script.sh   
foo
bar
~ % cat log.log 
foo
bar

This will log both stdout and stderr, with no side effects:

#!/usr/bin/env bash

{
        echo foo
        echo bar
        echo error >&2
} |& tee log.log

exit 0
~ % ./script.sh 
foo
bar
error
~ % cat log.log 
foo
bar
error

@bizmutowyszymon’s answer is just better if you want to log everything (the whole script, both stdout and stderr) both to the terminal and to the file; an advantage to using this method is that you can selectively decide which groups of commands to send to which:

#!/usr/bin/env bash

{
        echo This part shall be printed both to the terminal and to
        echo the
        echo file >&2
} |& tee log.log

echo But this part shall be printed only to the
echo terminal >&2

{ echo Here we output again to the both; } |& tee -a log.log

echo And this will just be printed to the file >>log.log

exit 0
~ % ./script.sh 
This part shall be printed both to the terminal and to
the
file
But this part shall be printed only to the
terminal
Here we output again to the both
~ % cat log.log 
This part shall be printed both to the terminal and to
the
file
Here we output again to the both
And this will just be printed to the file
Answered By: kos

You can put this line at the top of your script:

exec &> >(tee -a "update.out")

For example:

#!/usr/bin/env bash

exec &> >(tee -a "update.out")

echo "this is going through tee"
Answered By: bizmutowyszymon
Categories: Answers Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.