66

I have a bash file that I need to redirect all output to one file, debug log as well as to the terminal. I need to redirect both stdout and stderr to the debug and log it for all commands in the script.

I do not want to add 2>&1 | tee -a $DEBUG for every single command in the file. I could live with | tee -a $DEBUG.

I remember there was a way to do it with something like exec 2>&1.

Currently I'm using something like the following:

#!/bin/bash
DEBUGLOG=/tmp/debug
exec 2>&1
somecommand | tee -a $DEBUGLOG
somecommand2 | tee -a $DEBUGLOG
somecommand3 | tee -a $DEBUGLOG

but it does not work. Does anyone have a solution/can explain the cause?

1
  • 1
    In some shells, |& works as a shortcut for 2>&1 |, it's at least slightly more convenient.
    – Kevin
    Jan 20, 2013 at 18:12

6 Answers 6

63

You can use exec like this at the top of your script:

exec > >(tee "$HOME/somefile.log") 2>&1

For example:

#!/bin/bash -

exec > >(tee "$HOME/somefile.log") 2>&1

echo "$HOME"
echo hi
date
date +%F
echo bye 1>&2

Gives me output to the file $HOME/somefile.log and to the terminal like this:

/home/saml
hi
Sun Jan 20 13:54:17 EST 2013
2013-01-20
bye
8
  • 4
    Note that this uses bashisms -- it may not work in other shells (e.g., dash). But since the question specified bash, +1. Jan 21, 2013 at 20:39
  • 10
    @RichardHansen, process substitution is a feature that was introduced by ksh, not bash and is also supported by zsh so I wouldn't call it a bashism. Jan 23, 2013 at 14:55
  • 9
    @StephaneChazelas: You make a good point. I just wanted to point out that the syntax is not supported by the POSIX standard and thus won't universally work in /bin/sh scripts (many people erroneously use bash syntax in /bin/sh scripts). Jan 23, 2013 at 16:39
  • 1
    Is there a way to not redirect stderr except to the log file? If the original script is myscript and I run ./myscript > /dev/null, I should still see bye which comes from echo bye >&2. Oct 29, 2018 at 17:32
  • 1
    If someone has the same issue as @eun, I did and the root cause was that I used exec >(tee "$HOME/somefile.log") 2>&1 where exec > >(tee "$HOME/somefile.log") 2>&1 is needed. Apr 26, 2023 at 12:22
50

As for a solution to redirect lots of command at once:

#!/bin/bash
{
    somecommand 
    somecommand2
    somecommand3
} 2>&1 | tee -a $DEBUGLOG

Why your original solution does not work: exec 2>&1 will redirect the standard error output to the standard output of your shell, which, if you run your script from the console, will be your console. the pipe redirection on commands will only redirect the standard output of the command.

On the point of view of somecommand, its standard output goes into a pipe connected to tee and the standard error goes into the same file/pseudofile as the standard error of the shell, which you redirect to the standard output of the shell, which will be the console if you run your program from the console.

The one true way to explain it is to see what really happens:

Your shell's original environment might look like this if you run it from the terminal:

stdin -> /dev/pts/42
stdout -> /dev/pts/42
stderr -> /dev/pts/42

After you redirect standard error into standard output (exec 2>&1), you ... basically change nothing. But if you redirect the script's standard output to a file, you would end up with an environment like this:

stdin -> /dev/pts/42
stdout -> /your/file
stderr -> /dev/pts/42

Then redirecting the shell standard error into standard output would end up like this :

stdin -> /dev/pts/42
stdout -> /your/file
stderr -> /your/file

Running a command will inherit this environment. If you run a command and pipe it to tee, the command's environment would be :

stdin -> /dev/pts/42
stdout -> pipe:[4242]
stderr -> /your/file

So your command's standard error still goes into what the shell uses as its standard error.

You can actually see the environment of a command by looking in /proc/[pid]/fd: use ls -l to also list the symbolic link's content. The 0 file here is standard input, 1 is standard output and 2 is standard error. If the command opens more files (and most programs do), you will also see them. A program can also choose to redirect or close its standard input/output and reuse 0, 1 and 2.

1
  • +1 @BatchyX very informative
    – VanagaS
    Oct 27, 2021 at 7:41
4

Write stderr and stdout to a file, display stderr on screen (on stdout)

exec 2> >(tee -a -i "$HOME/somefile.log")
exec >> "$HOME/somefile.log"

Useful for crons, so you can receive errors (and only errors) by mail

2
  • But the OP appears to want the stdout to be written to the stdout, and your answer doesn’t do that. Jul 24, 2020 at 10:28
  • 3
    But this answer does do exactly what is needed by some people who googled for this. And for that, we thank you. Aug 31, 2020 at 22:16
1

Based on Capture all the output of a script to a file (from the script itself)

#!/usr/bin/env bash

if [ -z "$SCRIPT" ]; then
  export SCRIPT="$0"
  /usr/bin/script -a logfile.txt /bin/bash -c "$0 $*"
  exit 0
fi
echo
0

The script man page has some warnings:

It is not recommended to run script in non-interactive shells. The inner shell of script is always interactive, and this could lead to unexpected results.

You should also avoid use of script in command pipes, as script can read more input than you would expect.

I take this such that script is not safe to use when stdin and/or stdout are going to be redirected.

I'd to something like this when running commands:

output=$("<command> <args>" 2> tee -a $LOGFILE >&2)

It is understood that the OP doesn't want to do exactly that for every command but you can easily define a function

run () { ... output=$("$(@) 2> tee -a $LOGFILE >&2"; ... }

Which enables you to capture the regular output and working on it while logging the warnings and errors and also displaying them on stderr at the same time.

You could then do

[ $? -eq 0 ] && echo "<command> <args> succeeded! Output:\n$output\n(EOF)" >>$LOGFILE

to also log the regular output if desired.

0

All output goes to yourscript.sh.log in directory were script was called and also displays output in terminal.

#!/bin/bash

logfile=$(pwd)/${0##*/}.log
truncate -s0 "$logfile"
exec > >(tee -a "$logfile") 2>&1

!!IMPORTANT!! If you do not have #!/bin/bash on the first line, you may get a syntax error.

Credits: This is a modded version from one of the geniuses who works for Rocky Linux. Can't remember who. Thank you Rocky Linux Devs!

You must log in to answer this question.

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