Search code examples
bashshellstdoutstderrio-redirection

Send stdout + stderr to file, AND send stderr + arbitrary status messages to console


I dug through probably 20 questions across SO looking for a solution to this problem with no luck. It seems like a common task.

I have the following requirements for a bash script:

  1. Send stdout to log.out, NOT to console
  2. Send stderr to log.out AND to console
  3. Send arbitrary status messages to console (and to log.out?)

Currently I am attempting the following to achieve this:

#!/bin/bash

> log.out

exec 3>&1 4>&2
trap 'exec 2>&4 1>&3' 0 1 2 3
exec 1>>log.out 2>&1

# Test
echo "status goes to console" >&3
echo "stderr goes to file + console" >&2
echo "stdout goes to file"

My understanding of this code is roughly...

  1. Backup stdout to FD 3, and stderr to FD 4
  2. Reset them when the script exits (maybe unnecessary?)
  3. Send stdout to log.out, and send stderr there too

This works perfectly except that errors are not displayed to the console.

So, I thought, I'll just cat /dev/stderr to &3 in a separate bg process, and added this line under the 2nd exec:

cat /dev/stderr >&3 &

I don't understand why, but, this also sends stdout to &3, so my console reads:

echoes goes to console
stderr goes to file + console
stdout goes to file

I've tried probably 50 combinations without success. After much reading I am leaning towards needing a (custom?) C program or similar to achieve this, which seems kind of crazy to me.

Any help is greatly appreciated.


Thanks!

Hey thanks to @charles-duffy's comments and answers I was able to make a very small modification to my existing script that achieved the general idea of what I'm looking for:

exec 3>&1 4>&2
trap 'exec 2>&4 1>&3' 0 1 2 3
exec 1>log.out 2> >(tee /dev/tty >&1)

The downside is that messages do appear out-of-order in the log file.


Solution

  • Assuming you have bash 4.1 or later:

    # make copies of orig stdout, /dev/tty, and our log file FDs
    exec {orig_stdout_fd}>&1
    exec {tty_fd}>/dev/tty
    exec {file_fd}>log.out
    
    # ...and set them up however you wish, using ''tee'' for anything that goes two places
    exec >&$file_fd                                  # redirect stdout only to the log file
    exec 2> >(tee /dev/fd/"$tty_fd" >&$file_fd)      # redirect stderr to both sinks
    

    Note that writes to stderr will take longer than writes to stdout (since they're going through a FIFO), so ordering can be lost.