277

Is it possible to redirect all of the output of a Bourne shell script to somewhere, but with shell commands inside the script itself?

Redirecting the output of a single command is easy, but I want something more like this:

#!/bin/sh
if [ ! -t 0 ]; then
    # redirect all of my output to a file here
fi

# rest of script...

Meaning: if the script is run non-interactively (for example, cron), save off the output of everything to a file. If run interactively from a shell, let the output go to stdout as usual.

I want to do this for a script normally run by the FreeBSD periodic utility. It's part of the daily run, which I don't normally care to see every day in email, so I don't have it sent. However, if something inside this one particular script fails, that's important to me and I'd like to be able to capture and email the output of this one part of the daily jobs.

Update: Joshua's answer is spot-on, but I also wanted to save and restore stdout and stderr around the entire script, which is done like this:

# save stdout and stderr to file 
# descriptors 3 and 4, 
# then redirect them to "foo"
exec 3>&1 4>&2 >foo 2>&1

# ...

# restore stdout and stderr
exec 1>&3 2>&4
Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
Steve Madsen
  • 13,465
  • 4
  • 49
  • 67
  • 2
    Testing for $TERM is not the best way to test for interactive mode. Instead, test whether stdin is a tty (test -t 0). – C. K. Young Nov 24 '08 at 16:31
  • 2
    In other words: if [ ! -t 0 ]; then exec >somefile 2>&1; fi – C. K. Young Nov 24 '08 at 16:34
  • 1
    See here for all the goodness: [http://tldp.org/LDP/abs/html/io-redirection.html](http://tldp.org/LDP/abs/html/io-redirection.html) Basically what was said by Joshua. exec > file redirects stdout to a specific file, exec < file replaces stdin by file, etc. Its the same as usual but using exec (see man exec for more details). – Loki Nov 24 '08 at 16:39
  • 2
    In your update section, you should also close the FDs 3 and 4, like so: `exec 1>&3 2>&4 3>&- 4>&-` – Gurjeet Singh Oct 22 '16 at 13:28
  • `Permission denied` on the first `exec` line. – Vince May 03 '21 at 04:53

6 Answers6

258

Addressing the question as updated.

#...part of script without redirection...

{
    #...part of script with redirection...
} > file1 2>file2 # ...and others as appropriate...

#...residue of script without redirection...

The braces '{ ... }' provide a unit of I/O redirection. The braces must appear where a command could appear - simplistically, at the start of a line or after a semi-colon. (Yes, that can be made more precise; if you want to quibble, let me know.)

You are right that you can preserve the original stdout and stderr with the redirections you showed, but it is usually simpler for the people who have to maintain the script later to understand what's going on if you scope the redirected code as shown above.

The relevant sections of the Bash manual are Grouping Commands and I/O Redirection. The relevant sections of the POSIX shell specification are Compound Commands and I/O Redirection. Bash has some extra notations, but is otherwise similar to the POSIX shell specification.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • 5
    This is much clearer than saving the original descriptors and restoring them later. – Steve Madsen Apr 23 '10 at 19:41
  • 43
    I had to do some googling to understand what this is really doing, so I wanted to share. The curly braces become a ["block of code"](http://www.tldp.org/LDP/abs/html/special-chars.html#CODEBLOCKREF), which, in effect, creates an _anonymous function_. The output everything in the code block can then be redirected (See **Example 3-2** from that link). Also note that curly braces _do not_ launch a [subshell](http://www.tldp.org/LDP/abs/html/subshells.html), but similar [I/O redirects](http://www.tldp.org/LDP/abs/html/ioredirintro.html) _can_ be done with subshells using parentheses. – chris May 16 '16 at 21:54
  • 4
    I like this solution better than the others. Even a person with only the most basic understanding of I/O redirection can understand what's happening. Plus, it's more verbose. And, as a Pythoner, I love verbose. – John Red Nov 15 '16 at 09:42
  • 1
    Better do `>>`. Some people have the habit of `>`. Appending is always safer and more recommended than overw***ing. Somebody wrote an application which uses the standard copy command to export some data to the same destination. – neverMind9 Dec 28 '18 at 12:33
  • That app is ccc.bmw71(.pro) (3C Battery Monitor Widget, formerly “Battery Monitor Widget”) always exports the battery history to /sdcard/bmw_history.txt . If already exists, guess what, ***OVERWRITE!*** This made me accidentially lose some battery history. I set the limit in days from 30 to a very high number, which invalidated it and put it back to 30. I wanted to export the current battery history before importing the existing backup. Then that happened. – neverMind9 Dec 28 '18 at 12:35
  • Opinions differ. I prefer not to append in general, but it depends on the context. I also tend to avoid fixed name outputs, usually embedding the start time as part of the file name. – Jonathan Leffler Dec 28 '18 at 12:42
  • 6
    you could also use `{ //something } 2>&1 | tee outfile` to show console messages and output to file at the same time – HomeIsWhereThePcIs Sep 18 '19 at 20:20
  • Cool! This works with pipes also. { #do stuff } | less – Sagebrush Gardener Feb 28 '20 at 19:41
  • This solution does the redirection nicely simple. But where to put the conditional statement? – soundflix Sep 26 '22 at 19:24
  • @soundflix: which conditional statement? I'm sure the context of your question is clear to you, but it isn't to others. You can use `if [ …condition… ]; then { …; } >$file; fi` easily enough if that's what you're after — the braces can be indented like any other command. Spacing is important around the braces, and you need a newline or semicolon before the close brace. – Jonathan Leffler Sep 26 '22 at 20:43
  • @Jonathan Leffler: I was asking because the OP's question was how to make a redirection conditionally, which I thought was missing in your answer. Anyways, thanks, now it's clear to me. Thanks a lot! – soundflix Sep 27 '22 at 13:14
  • Is it somehow possible to use a variable instead of hardcoded value 'file1'? I tried to set the variable at the beginning of the script and then use for redirection it but without success. DIR="/tmp/logfile.log" { ... } >> $DIR – Vojtech Dec 21 '22 at 12:46
  • 1
    @Vojtech: yes, it's perfectly feasible to use a variable to hold the file names. Assuming there is a newline after the assignment and another before the close brace, what you outline should work. The braces are finicky; they must be separate words (spaced separately) and the close brace must appear where a new command could start. – Jonathan Leffler Dec 21 '22 at 13:28
232

Typically we would place one of these at or near the top of the script. Scripts that parse their command lines would do the redirection after parsing.

Send stdout to a file

exec > file

with stderr

exec > file                                                                      
exec 2>&1

append both stdout and stderr to file

exec >> file
exec 2>&1

As Jonathan Leffler mentioned in his comment:

exec has two separate jobs. The first one is to replace the currently executing shell (script) with a new program. The other is changing the I/O redirections in the current shell. This is distinguished by having no argument to exec.

Joachim Sauer
  • 302,674
  • 57
  • 556
  • 614
Joshua
  • 40,822
  • 8
  • 72
  • 132
  • 8
    I say also add 2>&1 to the end of that, just so stderr gets caught too. :-) – C. K. Young Nov 24 '08 at 16:32
  • 1
    Enlightened badge for fastest gun in the west years ago. – Joshua Aug 16 '11 at 15:34
  • You have no idea just how many Enlightened badges I got that way. :-P – C. K. Young Aug 16 '11 at 18:05
  • 9
    Where do you put these? At the top of the script? – colan Jul 23 '14 at 16:13
  • 5
    With this solution one has to also reset the redirect the script exits. The next answer by Jonathan Leffler is more "fail proof" in this sense. – Chuim Aug 27 '15 at 09:19
  • Which was not in the question until after the answer given. – Joshua Aug 27 '15 at 15:20
  • 1
    Can you explain what exactly happens when these commands are added to the beginning of a script? I had a completely different notion of what `exec` does. – John Red Nov 15 '16 at 09:41
  • 9
    @JohnRed: `exec` has two separate jobs. One is to replace the current script with another command, using the same process — you specify the other command as an argument to `exec` (and you can tweak I/O redirections as you do it). The other job is changing the I/O redirections in the current shell script without replacing it. This notation is distinguished by not having a command as an argument to `exec`. The notation in this answer is of the "I/O only" variant — it only changes the redirection and does not replace the script that's running. (The `set` command is similarly multi-purpose.) – Jonathan Leffler Nov 15 '16 at 14:05
  • 32
    `exec > >(tee -a "logs/logdata.log") 2>&1` prints the logs on the screen as well as writes them into a file – shriyog Feb 02 '17 at 09:20
  • Is there a way to buffer stdout output in a file this way and then print it to the screen? – unfa Nov 30 '17 at 13:29
  • @unfa: Not really. `tee` almost does what you ask for but hangs up if the screen blocks. – Joshua Nov 30 '17 at 17:16
  • I tried using `exec > stdout.txt` and then doing `echo stdout.txt`, but I get garbage. – unfa Dec 01 '17 at 11:51
  • 1
    @unfa: try `cat stdout.txt` instead of echo, see [man cat](https://linux.die.net/man/1/cat). Cat is the right command to print **content** of a file, echo prints a **string** to stdout! – Johannes Stadler Dec 14 '17 at 11:23
  • 1
    If the script is sourced, how would you restore stdout and stderr at the end? – Gauthier Jan 21 '21 at 19:40
  • @Gauthier: OP edited that into the question after I answered; and he edited in the answer as well. – Joshua Jan 21 '21 at 20:14
43

You can make the whole script a function like this:

main_function() {
  do_things_here
}

then at the end of the script have this:

if [ -z $TERM ]; then
  # if not run via terminal, log everything into a log file
  main_function 2>&1 >> /var/log/my_uber_script.log
else
  # run via terminal, only output to screen
  main_function
fi

Alternatively, you may log everything into logfile each run and still output it to stdout by simply doing:

# log everything, but also output to stdout
main_function 2>&1 | tee -a /var/log/my_uber_script.log
Daniel Serodio
  • 4,229
  • 5
  • 37
  • 33
dbguy
  • 459
  • 4
  • 4
  • 1
    Did you mean main_function >> /var/log/my_uber_script.log 2>&1 – Felipe Alvarez Feb 07 '12 at 05:02
  • I like using main_function in such pipe. But in this case your script does not return the original return value. In bash case you should exit then using 'exit ${PIPESTATUS[0]}'. – rudimeier Feb 27 '14 at 13:05
9

For saving the original stdout and stderr you can use:

exec [fd number]<&1 
exec [fd number]<&2

For example, the following code will print "walla1" and "walla2" to the log file (a.txt), "walla3" to stdout, "walla4" to stderr.

#!/bin/bash

exec 5<&1
exec 6<&2

exec 1> ~/a.txt 2>&1

echo "walla1"
echo "walla2" >&2
echo "walla3" >&5
echo "walla4" >&6
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Eyal leshem
  • 995
  • 2
  • 10
  • 21
  • 3
    Normally, it would be better to use `exec 5>&1` and `exec 6>&2`, using output redirection notation rather than input redirection notation for the outputs. You get away with it because when the script is run from a terminal, standard input is also writable and both standard output and standard error are readable by virtue (or is it 'vice'?) of a historical quirk: the terminal is opened for reading and writing and the same [open file description](http://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html) is used for all three standard I/O file descriptors. – Jonathan Leffler Nov 15 '16 at 14:17
4
[ -t <&0 ] || exec >> test.log
Thiem Nguyen
  • 6,345
  • 7
  • 30
  • 50
Dimitar
  • 41
  • 1
  • 2
    What does this do? – djdomi Sep 01 '21 at 05:43
  • The `[ -t ... ]` test is a condition which checks whether standard input is a terminal. The syntax `a ||b` says to run `a`, and then `b` if `a` failed. Thus the `exec` only takes place if standard input is not a terminal; so, if the script is run interactively, print all output to the terminal, but if not, print to a file. – tripleee Nov 14 '22 at 05:51
1

I finally figured out how to do it. I wanted to not just save the output to a file but also, find out if the bash script ran successfully or not!

I've wrapped the bash commands inside a function and then called the function main_function with a tee output to a file. Afterwards, I've captured the output using if [ $? -eq 0 ].

#! /bin/sh -

main_function() {
 python command.py
}

main_function > >(tee -a "/var/www/logs/output.txt") 2>&1

if [ $? -eq 0 ]
then
    echo 'Success!'
else
    echo 'Failure!'
fi
George Chalhoub
  • 14,968
  • 3
  • 38
  • 61
  • Your last conditional is ruining the script by forcing it to always return success. You'll want to capture the exit code and relay it to the caller. See also [Why is testing “$?” to see if a command succeeded or not, an anti-pattern?](https://stackoverflow.com/questions/36313216/why-is-testing-to-see-if-a-command-succeeded-or-not-an-anti-pattern) – tripleee Nov 14 '22 at 05:49