6

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?

0

2 Answers 2

10

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"
6
  • This is sleeker and, unlike my answer, doesn't have the adverse effect of changing the order of stdout / stderr when piping only stdout (albeit this will always log stderr), +1
    – kos
    Apr 19 at 8:47
  • 1
    &> is bash only (well, csh too, but not ksh). prefer: 2>&1 Apr 19 at 9:16
  • ... so does process substitution >(tee -a "update.out") and it might fail in other shells with some error like Syntax error: redirection unexpected. @OlivierDulac
    – Raffa
    Apr 19 at 14:10
  • @OlivierDulac Can you write the whole line? While the main answer works, I prefer to practice and remember more generally applicable options. I've tried: exec 2>&1 >(tee "update.out"), exec 2>&1 (tee "update.out"), exec 2>&1 | tee "update.out", and they don't work. PS: I'm using the #!/bin/bash line. Apr 20 at 17:38
  • 2
    @CarlosGonzález Have you tried exec > >(tee -a "update.out") 2>&1?
    – Raffa
    Apr 21 at 14:39
8

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
18
  • This works perfectly, thanks a lot, but there's a small quirk that I don't understand. For clarity to read the log I have some newlines "\n" in my echos. Using this solution, the output in both the terminal and the file shows "\n" instead of newlines. I can solve it easily with an empty echo, but it's a curious quirk. Apr 18 at 22:17
  • @CarlosGonzález How are you printing the newlines? Can you post an example of the echo commands you're using?
    – kos
    Apr 18 at 22:21
  • 1
    The interpretation of \n as a newline is implementation dependent - the bash builtin echo doesn't do so without the -e option. It's one of the reasons why printf is better than echo. Apr 18 at 22:39
  • Exaclty what @steeldriver said, I think that's what you're doing, either add -e to each echo command or, to not repeat yourself throughout the script, invoke shopt -s xpg_echo at the start of the script once to have echo automatically interpret backlash escapes.
    – kos
    Apr 18 at 22:42
  • 1
    Probably similarly a shell function is another possibility like update_all () { apt update; snap refresh; ...; } and then call the function piping its output to tee like update_all 2>&1 | tee file.log.
    – Raffa
    Apr 19 at 13:58

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .