Search code examples
bashfile-descriptortee

switch output file redirection in the middle of a bash script


I just want to save the output of my script at a place I don't know at the beginning of the script. I try something but I'm pretty sure that it's ugly. Is there an elegant way to do that:

#!/bin/bash

# Here I don't know where to write outputfile so I create a tmpfile
fic=$(mktemp -t)
trap 'rm -f $fic' EXIT
rm -f $fic
:> $fic
exec 3<> $fic
exec 1>&3
exec 2>&3

# some code in particular reading options and dest dir
echo foo
dir="."

# Here I finally know where I can write my output
fic2=$dir/log.log

cp -f $fic $fic2
exec 3>&- # close fd #3
exec 1>> $fic2
exec 2>&1

echo bar

Furthermore, I would like to tee the entire output, something like $ exec ... >(tee $fic)$ but I failed to find a solution.

Thanks a lot for any advice. PJLM


Solution

  • If you know that both output files are on the same file system, you can just mv the output file. The file descriptors you have open will continue to work.

    exec 1>/tmp/out1 2>&1
    echo out1
    mv /tmp/out1 /tmp/out2   # replace with your desired destination
    echo out2
    

    If you want to tee the output, and, again, both output files are on the same filesystem, you can do pretty much the same thing (once tee has opened the file for writing it will likewise keep writing to the same fd even if the file moves).

    log1=$(mktemp)
    exec 3>"$log1"
    exec 1> >(tee /dev/fd/3) 2>&1
    echo out1
    mv "$log1" "$log2"
    echo out2
    

    Note that instead of doing >(tee "$log1") I first open fd 3 in the shell and then use >(tee /dev/fd/3). This is because otherwise there is a potential race condition where tee will not have opened the file by the time we get to the mv step. (exec only waits until the subshell in which tee will run has started, but it takes some time for tee itself to start and open the file).


    If your first and second output files may not be on the same file system, you will have to do some more advanced shuffling and make sure that writing to the first file finishes before copying it.

    In the case of a simple redirect, we need to close the file descriptors before moving:

    exec 1>"$log1" 2>&1
    echo out1
    exec 1>&- 2>&-
    mv "$log1" "$log2"
    exec 1>>"$log2" 2>&1
    echo out2
    

    In the case of a process substitution with output files potentially on different file systems, we need to make sure that the process substitution finishes before moving the file:

    exec 3>&1 4>&2                # save original stdout, stderr
    exec 1> >(tee "$log1") 2>&1   # redirect to tee
    pid=$!                        # save pid of tee's subshell
    
    echo out1
    exec 1>&3 2>&4                # restore original stdout, stderr
    
    # wait until tee is done. on newer bash can use `wait $pid` instead
    while kill -0 $pid 2>/dev/null; do :; done
    
    mv "$log1" "$log2"
    
    # repeat steps above for new file
    exec 3>&1 4>&2
    exec 1> >(tee -a "$log2") 2>&1
    pid=$!
    echo out2
    exec 1>&3 2>&4
    while kill -0 $pid 2>/dev/null; do :; done