7

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?

2 Answers2

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"

  • 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 '24 at 08:47
  • 1
    &> is bash only (well, csh too, but not ksh). prefer: 2>&1 – Olivier Dulac Apr 19 '24 at 09: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 '24 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. – Carlos González Apr 20 '24 at 17:38
  • 2
    @CarlosGonzález Have you tried exec > >(tee -a "update.out") 2>&1? – Raffa Apr 21 '24 at 14:39
  • @Raffa Yes! That works perfectly – Carlos González Apr 22 '24 at 21:56
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
kos
  • 41,378
  • 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. – Carlos González Apr 18 '24 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 '24 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. – steeldriver Apr 18 '24 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 '24 at 22:42
  • @CarlosGonzález ^^ – kos Apr 18 '24 at 22:42
  • But I, too, prefer printf. Truth be told, you're better off just using printf "whatever\n"... – kos Apr 18 '24 at 22:44
  • I'm using echo without anything more, the thing is that I normally use zsh, and using echo in zsh, interprets by default "\n" as a newline, while regular bash doesn't. The braces don't matter really, the thing is that when I put the braces, I also added the #!/bin/bash line to the file and I didn't know that would make a difference in the output. Thanks to everyone for the suggestions @steeldriver @kos – Carlos González Apr 18 '24 at 23:51
  • @CarlosGonzález It's understandable, I too use zsh and sometimes fall for that. You're welcome! – kos Apr 18 '24 at 23:53
  • You will "lose" any errors: you could add a "2>&1" just before the "| tee" to make sure you get both stdout and stderr into the pipe. – Olivier Dulac Apr 19 '24 at 08:16
  • @CarlosGonzález : about echo/printf: always prefer printf, much much more portable and constant (across shells, and even across OSs). See https://unix.stackexchange.com/questions/65803/why-is-printf-better-than-echo and https://www.in-ulm.de/~mascheck/various/echo+printf/ . a basic echo "your text here" is easily replaced with : printf '%s\n' "your text here..." (there are multiple ways to do it, but it is a basic replacement and avoids problem if your text contains %. Usually put the "formatting and static" part in the first arg to printf, and the variable part in the 2nd (and others).) – Olivier Dulac Apr 19 '24 at 08:19
  • @OlivierDulac addressed that concern – kos Apr 19 '24 at 08:39
  • @kos +1 from me. – Olivier Dulac Apr 19 '24 at 09:12
  • actually, replace it with the proper: "2>&1 | tee", as "|& tee" is less portable (and OP mentionned using other shells than bash, as he mentions a ".sh" file which could be regular sh) ? sh, and ksh, does not have "|&" nor "&>" – Olivier Dulac Apr 19 '24 at 09:19
  • @OlivierDulac Added a bold caveat at the start of the answer, it shouldn't be of extreme concern here as Ubuntu comes with Bash by default, and people using other shells (often) know what they're doing, but certainly it was worth mentioning, thanks! – kos Apr 19 '24 at 09:22
  • @CarlosGonzález This answer has changed quite a bit, and you now have an excellent answer from bizmutowyszymon as well, you may want to check here again – kos Apr 19 '24 at 09:26
  • little nitpick: in your bold 1st sentence, you forgot the "|" after "2>&1" ^^ : "2>&1 |" – Olivier Dulac Apr 19 '24 at 09:28
  • @OlivierDulac Not so little, it was downright wrong :D thanks! – kos Apr 19 '24 at 09:30
  • 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 '24 at 13:58