en
ru
1 / 4

Posts: June 2016

  1. June 4, 2016 Copying script output to a logfile

    Sometimes there's a need to redirect stdout and stderr streams of a shell script to a logfile while preserving its output in the terminal. In such cases I used to wrap relevant portions of the script into compound commands and piping them to tee command. Below is a cleaner way to achieve the same results using named pipes.

    #!/usr/bin/env bash
    
    # stdout and stderr are redirected to this file.
    log_file="script.log"
    
    # Terminate script if any command results in unhandled error.
    set -e
    
    # There is no option to create a temporary fifo so we'll create a temporary
    # directory instead and put fifo inside of it.
    fifo_dir="$(mktemp -d)"
    fifo="${fifo_dir}/fifo"
    mkfifo "${fifo}"
    
    # `tee` command copies everything from fifo to both stdout and ${log_file}.
    tee "${log_file}" < "${fifo}" & tee_pid=$!
    
    # Redirect stdout and stderr to fifo.
    exec &> "${fifo}"
    
    cleanup() {
        # Remember exit code.
        code=$?
    
        # Allow cleanup code to finish even if error occurs.
        set +e
    
        echo "Exiting with code ${code}"
    
        # Close redirected stdout and stderr to finish `tee` process.
        exec 1>&- 2>&-
        wait ${tee_pid}
        rm -rf "${fifo_dir}"
    
        # Do something with generated log file, e.g.:
        # cat "${log_file}" | mail -s "${log_file}" user@example.com
    
        exit ${code}
    }
    
    # cleanup() will be called with appropriate exit code even if script finishes
    # due to an error (see `set -e` above).
    trap cleanup EXIT
    
    # Script body goes here.
    # Note that if the script process is replaced using `exec` at some point the
    # cleanup code will not be executed, i.e. temporary files will not be deleted.
    $@
    

    Closing output descriptors and waiting in cleanup() function is only needed to flush the logfile if we want to do something more with it inside of the script. Otherwise deleting the temporary directory containing the pipe is sufficient.

    There's an alternative way that uses builtin coproc command described here and some relevant discussion here. It has some benefits (e.g. not having to have access to a writable directory) but personally I'm sticking with named pipes for this purpose since the code looks more readable that way.