How do I write stderr to a file while using “tee” with a pipe?

前端 未结 10 942
独厮守ぢ
独厮守ぢ 2020-11-22 08:01

I know how to use tee to write the output (STDOUT) of aaa.sh to bbb.out, while still displaying it in the terminal:

相关标签:
10条回答
  • 2020-11-22 08:40

    In other words, you want to pipe stdout into one filter (tee bbb.out) and stderr into another filter (tee ccc.out). There is no standard way to pipe anything other than stdout into another command, but you can work around that by juggling file descriptors.

    { { ./aaa.sh | tee bbb.out; } 2>&1 1>&3 | tee ccc.out; } 3>&1 1>&2
    

    See also How to grep standard error stream (stderr)? and When would you use an additional file descriptor?

    In bash (and ksh and zsh), but not in other POSIX shells such as dash, you can use process substitution:

    ./aaa.sh > >(tee bbb.out) 2> >(tee ccc.out)
    

    Beware that in bash, this command returns as soon as ./aaa.sh finishes, even if the tee commands are still executed (ksh and zsh do wait for the subprocesses). This may be a problem if you do something like ./aaa.sh > >(tee bbb.out) 2> >(tee ccc.out); process_logs bbb.out ccc.out. In that case, use file descriptor juggling or ksh/zsh instead.

    0 讨论(0)
  • 2020-11-22 08:41

    why not simply:

    ./aaa.sh 2>&1 | tee -a log
    

    This simply redirects stderr to stdout, so tee echoes both to log and to screen. Maybe I'm missing something, because some of the other solutions seem really complicated.

    Note: Since bash version 4 you may use |& as an abbreviation for 2>&1 |:

    ./aaa.sh |& tee -a log
    
    0 讨论(0)
  • 2020-11-22 08:41

    Like the accepted answer well explained by lhunath, you can use

    command > >(tee -a stdout.log) 2> >(tee -a stderr.log >&2)
    

    Beware than if you use bash you could have some issue.

    Let me take the matthew-wilcoxson exemple.

    And for those who "seeing is believing", a quick test:

    (echo "Test Out";>&2 echo "Test Err") > >(tee stdout.log) 2> >(tee stderr.log >&2)
    

    Personally, when I try, I have this result :

    user@computer:~$ (echo "Test Out";>&2 echo "Test Err") > >(tee stdout.log) 2> >(tee stderr.log >&2)
    user@computer:~$ Test Out
    Test Err
    
    

    Both message does not appear at the same level. Why Test Out seem to be put like if it is my previous command ?
    Prompt is on a blank line, let me think the process is not finished, and when I press Enter this fix it.
    When I check the content of the files, it is ok, redirection works.

    Let take another test.

    function outerr() {
      echo "out"     # stdout
      echo >&2 "err" # stderr
    }
    
    user@computer:~$ outerr
    out
    err
    
    user@computer:~$ outerr >/dev/null
    err
    
    user@computer:~$ outerr 2>/dev/null
    out
    

    Trying again the redirection, but with this function.

    function test_redirect() {
      fout="stdout.log"
      ferr="stderr.log"
      echo "$ outerr"
      (outerr) > >(tee "$fout") 2> >(tee "$ferr" >&2)
      echo "# $fout content :"
      cat "$fout"
      echo "# $ferr content :"
      cat "$ferr"
    }
    

    Personally, I have this result :

    user@computer:~$ test_redirect
    $ outerr
    # stdout.log content :
    out
    out
    err
    # stderr.log content :
    err
    user@computer:~$
    

    No prompt on a blank line, but I don't see normal output, stdout.log content seem to be wrong, only stderr.log seem to be ok. If I relaunch it, output can be different...

    So, why ?

    Because, like explained here :

    Beware that in bash, this command returns as soon as [first command] finishes, even if the tee commands are still executed (ksh and zsh do wait for the subprocesses)

    So, if you use bash, prefer use the better exemple given in this other answer :

    { { outerr | tee "$fout"; } 2>&1 1>&3 | tee "$ferr"; } 3>&1 1>&2
    

    It will fix the previous issues.

    Now, the question is, how to retrieve exit status code ?
    $? does not works.
    I have no found better solution than switch on pipefail with set -o pipefail (set +o pipefail to switch off) and use ${PIPESTATUS[0]} like this

    function outerr() {
      echo "out"
      echo >&2 "err"
      return 11
    }
    
    function test_outerr() {
      local - # To preserve set option
      ! [[ -o pipefail ]] && set -o pipefail; # Or use second part directly
      local fout="stdout.log"
      local ferr="stderr.log"
      echo "$ outerr"
      { { outerr | tee "$fout"; } 2>&1 1>&3 | tee "$ferr"; } 3>&1 1>&2
      # First save the status or it will be lost
      local status="${PIPESTATUS[0]}" # Save first, the second is 0, perhaps tee status code.
      echo "==="
      echo "# $fout content :"
      echo "<==="
      cat "$fout"
      echo "===>"
      echo "# $ferr content :"
      echo "<==="
      cat "$ferr"
      echo "===>"
      if (( status > 0 )); then
        echo "Fail $status > 0"
        return "$status" # or whatever
      fi
    }
    
    user@computer:~$ test_outerr
    $ outerr
    err
    out
    ===
    # stdout.log content :
    <===
    out
    ===>
    # stderr.log content :
    <===
    err
    ===>
    Fail 11 > 0
    
    0 讨论(0)
  • 2020-11-22 08:49

    If using bash:

    # Redirect standard out and standard error separately
    % cmd >stdout-redirect 2>stderr-redirect
    
    # Redirect standard error and out together
    % cmd >stdout-redirect 2>&1
    
    # Merge standard error with standard out and pipe
    % cmd 2>&1 |cmd2
    

    Credit (not answering from the top of my head) goes here: http://www.cygwin.com/ml/cygwin/2003-06/msg00772.html

    0 讨论(0)
  • 2020-11-22 08:50

    This may be useful for people finding this via google. Simply uncomment the example you want to try out. Of course, feel free to rename the output files.

    #!/bin/bash
    
    STATUSFILE=x.out
    LOGFILE=x.log
    
    ### All output to screen
    ### Do nothing, this is the default
    
    
    ### All Output to one file, nothing to the screen
    #exec > ${LOGFILE} 2>&1
    
    
    ### All output to one file and all output to the screen
    #exec > >(tee ${LOGFILE}) 2>&1
    
    
    ### All output to one file, STDOUT to the screen
    #exec > >(tee -a ${LOGFILE}) 2> >(tee -a ${LOGFILE} >/dev/null)
    
    
    ### All output to one file, STDERR to the screen
    ### Note you need both of these lines for this to work
    #exec 3>&1
    #exec > >(tee -a ${LOGFILE} >/dev/null) 2> >(tee -a ${LOGFILE} >&3)
    
    
    ### STDOUT to STATUSFILE, stderr to LOGFILE, nothing to the screen
    #exec > ${STATUSFILE} 2>${LOGFILE}
    
    
    ### STDOUT to STATUSFILE, stderr to LOGFILE and all output to the screen
    #exec > >(tee ${STATUSFILE}) 2> >(tee ${LOGFILE} >&2)
    
    
    ### STDOUT to STATUSFILE and screen, STDERR to LOGFILE
    #exec > >(tee ${STATUSFILE}) 2>${LOGFILE}
    
    
    ### STDOUT to STATUSFILE, STDERR to LOGFILE and screen
    #exec > ${STATUSFILE} 2> >(tee ${LOGFILE} >&2)
    
    
    echo "This is a test"
    ls -l sdgshgswogswghthb_this_file_will_not_exist_so_we_get_output_to_stderr_aronkjegralhfaff
    ls -l ${0}
    
    0 讨论(0)
  • 2020-11-22 08:54

    The following will work for KornShell(ksh) where the process substitution is not available,

    # create a combined(stdin and stdout) collector
    exec 3 <> combined.log
    
    # stream stderr instead of stdout to tee, while draining all stdout to the collector
    ./aaa.sh 2>&1 1>&3 | tee -a stderr.log 1>&3
    
    # cleanup collector
    exec 3>&-
    

    The real trick here, is the sequence of the 2>&1 1>&3 which in our case redirects the stderr to stdout and redirects the stdout to descriptor 3. At this point the stderr and stdout are not combined yet.

    In effect, the stderr(as stdin) is passed to tee where it logs to stderr.log and also redirects to descriptor 3.

    And descriptor 3 is logging it to combined.log all the time. So the combined.log contains both stdout and stderr.

    0 讨论(0)
提交回复
热议问题