Redirect stderr and stdout in Bash

2018-12-31 18:01发布

问题:

I want to redirect both stdout and stderr of a process to a single file. How do I do that in Bash?

回答1:

Take a look here. Should be:

yourcommand &>filename

(redirects both stdout and stderr to filename).



回答2:

do_something 2>&1 | tee -a some_file

This is going to redirect stderr to stdout and stdout to some_file and print it to stdout.



回答3:

You can redirect stderr to stdout and the stdout into a file:

some_command >file.log 2>&1 

See http://tldp.org/LDP/abs/html/io-redirection.html

This format is preferred than the most popular &> format that only work in bash. In Bourne shell it could be interpreted as running the command in background. Also the format is more readable 2 (is STDERR) redirected to 1 (STDOUT).

EDIT: changed the order as pointed out in the comments



回答4:

# Close STDOUT file descriptor
exec 1<&-
# Close STDERR FD
exec 2<&-

# Open STDOUT as $LOG_FILE file for read and write.
exec 1<>$LOG_FILE

# Redirect STDERR to STDOUT
exec 2>&1

echo \"This line will appear in $LOG_FILE, not \'on screen\'\"

Now, simple echo will write to $LOG_FILE. Useful for daemonizing.

To the author of the original post,

It depends what you need to achieve. If you just need to redirect in/out of a command you call from your script, the answers are already given. Mine is about redirecting within current script which affects all commands/built-ins(includes forks) after the mentioned code snippet.


Another cool solution is about redirecting to both std-err/out AND to logger or log file at once which involves splitting \"a stream\" into two. This functionality is provided by \'tee\' command which can write/append to several file descriptors(files, sockets, pipes, etc) at once: tee FILE1 FILE2 ... >(cmd1) >(cmd2) ...

exec 3>&1 4>&2 1> >(tee >(logger -i -t \'my_script_tag\') >&3) 2> >(tee >(logger -i -t \'my_script_tag\') >&4)
trap \'cleanup\' INT QUIT TERM EXIT


get_pids_of_ppid() {
    local ppid=\"$1\"

    RETVAL=\'\'
    local pids=`ps x -o pid,ppid | awk \"\\\\$2 == \\\\\"$ppid\\\\\" { print \\\\$1 }\"`
    RETVAL=\"$pids\"
}


# Needed to kill processes running in background
cleanup() {
    local current_pid element
    local pids=( \"$$\" )

    running_pids=(\"${pids[@]}\")

    while :; do
        current_pid=\"${running_pids[0]}\"
        [ -z \"$current_pid\" ] && break

        running_pids=(\"${running_pids[@]:1}\")
        get_pids_of_ppid $current_pid
        local new_pids=\"$RETVAL\"
        [ -z \"$new_pids\" ] && continue

        for element in $new_pids; do
            running_pids+=(\"$element\")
            pids=(\"$element\" \"${pids[@]}\")
        done
    done

    kill ${pids[@]} 2>/dev/null
}

So, from the beginning. Let\'s assume we have terminal connected to /dev/stdout(FD #1) and /dev/stderr(FD #2). In practice, it could be a pipe, socket or whatever.

  • Create FDs #3 and #4 and point to the same \"location\" as #1 and #2 respectively. Changing FD #1 doesn\'t affect FD #3 from now on. Now, FDs #3 and #4 point to STDOUT and STDERR respectively. These will be used as real terminal STDOUT and STDERR.
  • 1> >(...) redirects STDOUT to command in parens
  • parens(sub-shell) executes \'tee\' reading from exec\'s STDOUT(pipe) and redirects to \'logger\' command via another pipe to sub-shell in parens. At the same time it copies the same input to FD #3(terminal)
  • the second part, very similar, is about doing the same trick for STDERR and FDs #2 and #4.

The result of running a script having the above line and additionally this one:

echo \"Will end up in STDOUT(terminal) and /var/log/messages\"

...is as follows:

$ ./my_script
Will end up in STDOUT(terminal) and /var/log/messages

$ tail -n1 /var/log/messages
Sep 23 15:54:03 wks056 my_script_tag[11644]: Will end up in STDOUT(terminal) and /var/log/messages

If you want to see clearer picture, add these 2 lines to the script:

ls -l /proc/self/fd/
ps xf


回答5:

bash your_script.sh 1>file.log 2>&1

1>file.log instructs the shell to send STDOUT to the file file.log, and 2>&1 tells it to redirect STDERR (file descriptor 2) to STDOUT (file descriptor 1).

Note: The order matters as liw.fi pointed out, 2>&1 1>file.log doesn\'t work.



回答6:

Curiously, this works:

yourcommand &> filename

But this gives a syntax error:

yourcommand &>> filename
syntax error near unexpected token `>\'

You have to use:

yourcommand 1>> filename 2>&1


回答7:

Short answer: Command >filename 2>&1 or Command &>filename


Explanation:

Consider the following code which prints the word \"stdout\" to stdout and the word \"stderror\" to stderror.

$ (echo \"stdout\"; echo \"stderror\" >&2)
stdout
stderror

Note that the \'&\' operator tells bash that 2 is a file descriptor (which points to the stderr) and not a file name. If we left out the \'&\', this command would print stdout to stdout, and create a file named \"2\" and write stderror there.

By experimenting with the code above, you can see for yourself exactly how redirection operators work. For instance, by changing which file which of the two descriptors 1,2, is redirected to /dev/null the following two lines of code delete everything from the stdout, and everything from stderror respectively (printing what remains).

$ (echo \"stdout\"; echo \"stderror\" >&2) 1>/dev/null
stderror
$ (echo \"stdout\"; echo \"stderror\" >&2) 2>/dev/null
stdout

Now, we can explain why the solution why the following code produces no output:

(echo \"stdout\"; echo \"stderror\" >&2) >/dev/null 2>&1

To truly understand this, I highly recommend you read this webpage on file descriptor tables. Assuming you have done that reading, we can proceed. Note that Bash processes left to right; thus Bash sees >/dev/null first (which is the same as 1>/dev/null), and sets the file descriptor 1 to point to /dev/null instead of the stdout. Having done this, Bash then moves rightwards and sees 2>&1. This sets the file descriptor 2 to point to the same file as file descriptor 1 (and not to file descriptor 1 itself!!!! (see this resource on pointers for more info)) . Since file descriptor 1 points to /dev/null, and file descriptor 2 points to the same file as file descriptor 1, file descriptor 2 now also points to /dev/null. Thus both file descriptors point to /dev/null, and this is why no output is rendered.


To test if you really understand the concept, try to guess the output when we switch the redirection order:

(echo \"stdout\"; echo \"stderror\" >&2)  2>&1 >/dev/null

stderror

The reasoning here is that evaluating from left to right, Bash sees 2>&1, and thus sets the file descriptor 2 to point to the same place as file descriptor 1, ie stdout. It then sets file descriptor 1 (remember that >/dev/null = 1>/dev/null) to point to >/dev/null, thus deleting everything which would usually be send to to the standard out. Thus all we are left with was that which was not send to stdout in the subshell (the code in the parentheses)- i.e. \"stderror\". The interesting thing to note there is that even though 1 is just a pointer to the stdout, redirecting pointer 2 to 1 via 2>&1 does NOT form a chain of pointers 2 -> 1 -> stdout. If it did, as a result of redirecting 1 to /dev/null, the code 2>&1 >/dev/null would give the pointer chain 2 -> 1 -> /dev/null, and thus the code would generate nothing, in contrast to what we saw above.


Finally, I\'d note that there is a simpler way to do this:

From section 3.6.4 here, we see that we can use the operator &> to redirect both stdout and stderr. Thus, to redirect both the stderr and stdout output of any command to \\dev\\null (which deletes the output), we simply type $ command &> /dev/null or in case of my example:

$ (echo \"stdout\"; echo \"stderror\" >&2) &>/dev/null

Key takeaways:

  • File descriptors behave like pointers (although file descriptors are not the same as file pointers)
  • Redirecting a file descriptor \"a\" to a file descriptor \"b\" which points to file \"f\", causes file descriptor \"a\" to point to the same place as file descriptor b - file \"f\". It DOES NOT form a chain of pointers a -> b -> f
  • Because of the above, order matters, 2>&1 >/dev/null is != >/dev/null 2>&1. One generates output and the other does not!

Finally have a look at these great resources:

Bash Documentation on Redirection, An Explanation of File Descriptor Tables, Introduction to Pointers



回答8:

LOG_FACILITY=\"local7.notice\"
LOG_TOPIC=\"my-prog-name\"
LOG_TOPIC_OUT=\"$LOG_TOPIC-out[$$]\"
LOG_TOPIC_ERR=\"$LOG_TOPIC-err[$$]\"

exec 3>&1 > >(tee -a /dev/fd/3 | logger -p \"$LOG_FACILITY\" -t \"$LOG_TOPIC_OUT\" )
exec 2> >(logger -p \"$LOG_FACILITY\" -t \"$LOG_TOPIC_ERR\" )

It is related: Writing stdOut & stderr to syslog.

It almost work, but not from xinted ;(



回答9:

I wanted a solution to have the output from stdout plus stderr written into a log file and stderr still on console. So I needed to duplicate the stderr output via tee.

This is the solution I found:

command 3>&1 1>&2 2>&3 1>>logfile | tee -a logfile
  • First swap stderr and stdout
  • then append the stdout to the log file
  • pipe stderr to tee and append it also to the log file


回答10:

For situation, when \"piping\" is necessary you can use :

|&

For example:

echo -ne \"15\\n100\\n\"|sort -c |& tee >sort_result.txt

or

TIMEFORMAT=%R;for i in `seq 1 20` ; do time kubectl get pods |grep node >>js.log  ; done |& sort -h

This bash-based solutions can pipe STDOUT and STDERR separately (from STDERR of \"sort -c\" or from STDERR to \"sort -h\").



回答11:

\"Easiest\" way (bash4 only): ls * 2>&- 1>&-.



回答12:

The following functions can be used to automate the process of toggling outputs beetwen stdout/stderr and a logfile.

#!/bin/bash

    #set -x

    # global vars
    OUTPUTS_REDIRECTED=\"false\"
    LOGFILE=/dev/stdout

    # \"private\" function used by redirect_outputs_to_logfile()
    function save_standard_outputs {
        if [ \"$OUTPUTS_REDIRECTED\" == \"true\" ]; then
            echo \"[ERROR]: ${FUNCNAME[0]}: Cannot save standard outputs because they have been redirected before\"
            exit 1;
        fi
        exec 3>&1
        exec 4>&2

        trap restore_standard_outputs EXIT
    }

    # Params: $1 => logfile to write to
    function redirect_outputs_to_logfile {
        if [ \"$OUTPUTS_REDIRECTED\" == \"true\" ]; then
            echo \"[ERROR]: ${FUNCNAME[0]}: Cannot redirect standard outputs because they have been redirected before\"
            exit 1;
        fi
        LOGFILE=$1
        if [ -z \"$LOGFILE\" ]; then
            echo \"[ERROR]: ${FUNCNAME[0]}: logfile empty [$LOGFILE]\"

        fi
        if [ ! -f $LOGFILE ]; then
            touch $LOGFILE
        fi
        if [ ! -f $LOGFILE ]; then
            echo \"[ERROR]: ${FUNCNAME[0]}: creating logfile [$LOGFILE]\"
            exit 1
        fi

        save_standard_outputs

        exec 1>>${LOGFILE%.log}.log
        exec 2>&1
        OUTPUTS_REDIRECTED=\"true\"
    }

    # \"private\" function used by save_standard_outputs() 
    function restore_standard_outputs {
        if [ \"$OUTPUTS_REDIRECTED\" == \"false\" ]; then
            echo \"[ERROR]: ${FUNCNAME[0]}: Cannot restore standard outputs because they have NOT been redirected\"
            exit 1;
        fi
        exec 1>&-   #closes FD 1 (logfile)
        exec 2>&-   #closes FD 2 (logfile)
        exec 2>&4   #restore stderr
        exec 1>&3   #restore stdout

        OUTPUTS_REDIRECTED=\"false\"
    }

Example of usage inside script:

echo \"this goes to stdout\"
redirect_outputs_to_logfile /tmp/one.log
echo \"this goes to logfile\"
restore_standard_outputs 
echo \"this goes to stdout\"


回答13:

For tcsh, I have to use the following command :

command >& file

If use command &> file , it will give \"Invalid null command\" error.



回答14:

@fernando-fabreti

Adding to what you did I changed the functions slightly and removed the &- closing and it worked for me.

    function saveStandardOutputs {
      if [ \"$OUTPUTS_REDIRECTED\" == \"false\" ]; then
        exec 3>&1
        exec 4>&2
        trap restoreStandardOutputs EXIT
      else
          echo \"[ERROR]: ${FUNCNAME[0]}: Cannot save standard outputs because they have been redirected before\"
          exit 1;
      fi
  }

  # Params: $1 => logfile to write to
  function redirectOutputsToLogfile {
      if [ \"$OUTPUTS_REDIRECTED\" == \"false\" ]; then
        LOGFILE=$1
        if [ -z \"$LOGFILE\" ]; then
            echo \"[ERROR]: ${FUNCNAME[0]}: logfile empty [$LOGFILE]\"
        fi
        if [ ! -f $LOGFILE ]; then
            touch $LOGFILE
        fi
        if [ ! -f $LOGFILE ]; then
            echo \"[ERROR]: ${FUNCNAME[0]}: creating logfile [$LOGFILE]\"
            exit 1
        fi
        saveStandardOutputs
        exec 1>>${LOGFILE}
        exec 2>&1
        OUTPUTS_REDIRECTED=\"true\"
      else
        echo \"[ERROR]: ${FUNCNAME[0]}: Cannot redirect standard outputs because they have been redirected before\"
          exit 1;
      fi
  }
  function restoreStandardOutputs {
      if [ \"$OUTPUTS_REDIRECTED\" == \"true\" ]; then
      exec 1>&3   #restore stdout
      exec 2>&4   #restore stderr
      OUTPUTS_REDIRECTED=\"false\"
     fi
  }
  LOGFILE_NAME=\"tmp/one.log\"
  OUTPUTS_REDIRECTED=\"false\"

  echo \"this goes to stdout\"
  redirectOutputsToLogfile $LOGFILE_NAME
  echo \"this goes to logfile\"
  echo \"${LOGFILE_NAME}\"
  restoreStandardOutputs 
  echo \"After restore this goes to stdout\"