bash 如何让 STDOUT 和 STDERR 都转到终端和日志文件?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/363223/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-09 17:50:31  来源:igfitidea点击:

How do I get both STDOUT and STDERR to go to the terminal and a log file?

bashloggingshell

提问by JPLemme

I have a script which will be run interactively by non-technical users. The script writes status updates to STDOUT so that the user can be sure that the script is running OK.

我有一个将由非技术用户交互运行的脚本。脚本将状态更新写入 STDOUT,以便用户可以确保脚本运行正常。

I want both STDOUT and STDERR redirected to the terminal (so that the user can see that the script is working as well as see if there was a problem). I also want both streams redirected to a log file.

我希望 STDOUT 和 STDERR 都重定向到终端(以便用户可以看到脚本正在运行以及查看是否有问题)。我还希望两个流都重定向到日志文件。

I've seen a bunch of solutions on the net. Some don't work and others are horribly complicated. I've developed a workable solution (which I'll enter as an answer), but it's kludgy.

我在网上看到了一堆解决方案。有些不起作用,有些则非常复杂。我已经开发了一个可行的解决方案(我将其作为答案输入),但它很笨拙。

The perfect solution would be a single line of code that could be incorporated into the beginning of any script that sends both streams to both the terminal and a log file.

完美的解决方案是一行代码,可以将其合并到任何将流发送到终端和日志文件的任何脚本的开头。

EDIT:Redirecting STDERR to STDOUT and piping the result to tee works, but it depends on the users remembering to redirect and pipe the output. I want the logging to be fool-proof and automatic (which is why I'd like to be able to embed the solution into the script itself.)

编辑:将 STDERR 重定向到 STDOUT 并将结果通过管道传输到 tee 工作,但这取决于用户记得重定向和管道输出。我希望日志记录是万无一失的和自动的(这就是为什么我希望能够将解决方案嵌入到脚本本身中。)

回答by Paul Tomblin

Use "tee" to redirect to a file and the screen. Depending on the shell you use, you first have to redirect stderr to stdout using

使用“tee”重定向到文件和屏幕。根据您使用的 shell,您首先必须使用以下命令将 stderr 重定向到 stdout

./a.out 2>&1 | tee output

or

或者

./a.out |& tee output

In csh, there is a built-in command called "script" that will capture everything that goes to the screen to a file. You start it by typing "script", then doing whatever it is you want to capture, then hit control-D to close the script file. I don't know of an equivalent for sh/bash/ksh.

在 csh 中,有一个名为“script”的内置命令,可以将进入屏幕的所有内容捕获到文件中。您通过键入“脚本”开始它,然后执行您想要捕获的任何操作,然后按 control-D 关闭脚本文件。我不知道 sh/bash/ksh 的等价物。

Also, since you have indicated that these are your own sh scripts that you can modify, you can do the redirection internally by surrounding the whole script with braces or brackets, like

此外,由于您已指出这些是您自己的 sh 脚本,您可以对其进行修改,因此您可以通过用大括号或方括号包围整个脚本来在内部进行重定向,例如

  #!/bin/sh
  {
    ... whatever you had in your script before
  } 2>&1 | tee output.file

回答by Jason Sydes

Approaching half a decade later...

接近五年后......

I believe this is the "perfect solution" sought by the OP.

我相信这是 OP 寻求的“完美解决方案”。

Here's a one liner you can add to the top of your Bash script:

这是您可以添加到 Bash 脚本顶部的单行代码:

exec > >(tee -a $HOME/logfile) 2>&1

Here's a small script demonstrating its use:

这是一个演示其用法的小脚本:

#!/usr/bin/env bash

exec > >(tee -a $HOME/logfile) 2>&1

# Test redirection of STDOUT
echo test_stdout

# Test redirection of STDERR
ls test_stderr___this_file_does_not_exist

(Note: This only works with Bash. It will notwork with /bin/sh.)

(注意:这只适用于 Bash。它不适用于 /bin/sh。)

Adapted from here; the original did not, from what I can tell, catch STDERR in the logfile. Fixed with a note from here.

改编自这里;据我所知,原始文件并没有在日志文件中捕获 STDERR。用这里的注释修复。

回答by MatrixManAtYrService

The Pattern

模式

the_cmd 1> >(tee stdout.txt ) 2> >(tee stderr.txt >&2 )

This redirects both stdout and stderr separately, and it sends separatecopies of stdout and stderr to the caller (which might be your terminal).

这会分别重定向 stdout 和 stderr,并将stdout 和 stderr 的单独副本发送给调用者(可能是您的终端)。

  • In zsh, it will not proceed to the next statement until the tees have finished.

  • In bash, you may find that the final few lines of output appear afterwhatever statement comes next.

  • 在 zsh 中,在tees 完成之前,它不会继续执行下一条语句。

  • 在 bash 中,您可能会发现最后几行输出出现接下来出现的任何语句之后。

In either case, the right bits go to the right places.

在任何一种情况下,正确的位都到正确的地方。



Explanation

解释

Here's a script (stored in ./example):

这是一个脚本(存储在 ./example 中):

#! /usr/bin/env bash
the_cmd()
{
    echo out;
    1>&2 echo err;
}

the_cmd 1> >(tee stdout.txt ) 2> >(tee stderr.txt >&2 )

Here's a session:

这是一个会话:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

Here's how it works:

这是它的工作原理:

  1. Both teeprocesses are started, their stdins are assigned to file descriptors. Because they're enclosed in process substitutions, the paths to those file descriptors are substituted in the calling command, so now it looks something like this:
  1. 两个tee进程都启动了,它们的标准输入被分配给文件描述符。因为它们包含在进程替换中,这些文件描述符的路径在调用命令中被替换,所以现在看起来像这样:

the_cmd 1> /proc/self/fd/13 2> /proc/self/fd/14

the_cmd 1> /proc/self/fd/13 2> /proc/self/fd/14

  1. the_cmdruns, writing stdout to the first file descriptor, and stderr to the second one.

  2. In the bash case, once the_cmdfinishes, the following statement happens immediately (if your terminal is the caller, then you will see your prompt appear).

  3. In the zsh case, once the_cmdfinishes, the shell waits for both of the teeprocesses to finish before moving on. More on this here.

  4. The first teeprocess, which is reading from the_cmd's stdout, writes a copy of that stdout back to the caller because that's what teedoes. Its outputs are not redirected, so they make it back to the caller unchanged

  5. The second teeprocess has it's stdoutredirected to the caller's stderr(which is good, because it's stdin is reading from the_cmd's stderr). So when it writes to its stdout, those bits go to the caller's stderr.

  1. the_cmd运行,将 stdout 写入第一个文件描述符,将 stderr 写入第二个。

  2. 在 bash 情况下,一旦the_cmd完成,以下语句立即发生(如果您的终端是调用者,那么您将看到提示出现)。

  3. 在 zsh 的情况下,一旦the_cmd完成,shell 会等待两个tee进程都完成后再继续。更多关于这里

  4. 第一个tee进程从the_cmd的标准输出中读取,将标准输出的副本写回给调用者,因为这就是tee它的作用。它的输出不会被重定向,所以它们会原封不动地返回给调用者

  5. 第二个tee进程将它stdout重定向到调用者的stderr(这很好,因为它的 stdin 正在从the_cmd的 stderr读取)。因此,当它写入其标准输出时,这些位将进入调用者的标准错误。

This keeps stderr separate from stdout both in the files and in the command's output.

这使文件和命令输出中的 stderr 与 stdout 分开。

If the first tee writes any errors, they'll show up in both the stderr file and in the command's stderr, if the second tee writes any errors, they'll only show up only in the terminal's stderr.

如果第一个 tee 写入任何错误,它们将同时显示在 stderr 文件和命令的 stderr 中,如果第二个 tee 写入任何错误,它们将仅显示在终端的 stderr 中。

回答by flolo

the to redirect stderr to stdout append this at your command: 2>&1For outputting to terminal and logging into file you should use tee

将 stderr 重定向到 stdout 在您的命令中附加此:2>&1对于输出到终端并登录到文件,您应该使用tee

Both together would look like this:

两者放在一起看起来像这样:

 mycommand 2>&1 | tee mylogfile.log

EDIT: For embedding into your script you would do the same. So your script

编辑:为了嵌入到你的脚本中,你会做同样的事情。所以你的脚本

#!/bin/sh
whatever1
whatever2
...
whatever3

would end up as

最终会变成

#!/bin/sh
( whatever1
whatever2
...
whatever3 ) 2>&1 | tee mylogfile.log

回答by Don Hatch

EDIT: I see I got derailed and ended up answering a different question from the one asked. The answer to the real question is at the bottom of Paul Tomblin's answer. (If you want to enhance that solution to redirect stdout and stderr separately for some reason, you could use the technique I describe here.)

编辑:我看到我出轨了,最后回答了一个与提出的问题不同的问题。真正问题的答案在 Paul Tomblin 答案的底部。(如果您想增强该解决方案以出于某种原因分别重定向 stdout 和 stderr,您可以使用我在此处描述的技术。)



I've been wanting an answer that preserves the distinction between stdout and stderr. Unfortunately all of the answers given so far that preserve that distinction are race-prone: they risk programs seeing incomplete input, as I pointed out in comments.

我一直想要一个保留标准输出和标准错误之间区别的答案。不幸的是,到目前为止,所有保留这种区别的答案都容易出现竞争:正如我在评论中指出的那样,它们冒着程序看到不完整输入的风险。

I think I finally found an answer that preserves the distinction, is not race prone, and isn't terribly fiddly either.

我想我终于找到了一个保留这种区别的答案,既不偏向于种族,也不十分繁琐。

First building block: to swap stdout and stderr:

第一个构建块:交换 stdout 和 stderr:

my_command 3>&1 1>&2 2>&3-

Second building block: if we wanted to filter (e.g. tee) only stderr, we could accomplish that by swapping stdout&stderr, filtering, and then swapping back:

第二个构建块:如果我们只想过滤(例如 tee)stderr,我们可以通过交换 stdout&stderr、过滤然后交换回来来实现:

{ my_command 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3-

Now the rest is easy: we can add a stdout filter, either at the beginning:

现在剩下的就很简单了:我们可以在开头添加一个标准输出过滤器:

{ { my_command | stdout_filter;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3-

or at the end:

或最后:

{ my_command 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filter

To convince myself that both of the above commands work, I used the following:

为了让自己相信上述两个命令都有效,我使用了以下命令:

alias my_command='{ echo "to stdout"; echo "to stderr" >&2;}'
alias stdout_filter='{ sleep 1; sed -u "s/^/teed stdout: /" | tee stdout.txt;}'
alias stderr_filter='{ sleep 2; sed -u "s/^/teed stderr: /" | tee stderr.txt;}'

Output is:

输出是:

...(1 second pause)...
teed stdout: to stdout
...(another 1 second pause)...
teed stderr: to stderr

and my prompt comes back immediately after the "teed stderr: to stderr", as expected.

teed stderr: to stderr正如预期的那样,我的提示在“ ”之后立即返回。

Footnote about zsh:

关于 zsh 的脚注

The above solution works in bash (and maybe some other shells, I'm not sure), but it doesn't work in zsh. There are two reasons it fails in zsh:

上述解决方案适用于 bash(也许还有其他一些 shell,我不确定),但它不适用于 zsh。在 zsh 中失败有两个原因:

  1. the syntax 2>&3-isn't understood by zsh; that has to be rewritten as 2>&3 3>&-
  2. in zsh (unlike other shells), if you redirect a file descriptor that's already open, in some cases (I don't completely understand how it decides) it does a built-in tee-like behavior instead. To avoid this, you have to close each fd prior to redirecting it.
  1. 2>&3-zsh 不理解语法;必须改写为2>&3 3>&-
  2. 在 zsh 中(与其他 shell 不同),如果您重定向已经打开的文件描述符,在某些情况下(我不完全理解它是如何决定的),它会执行内置的类似 tee 的行为。为避免这种情况,您必须在重定向之前关闭每个 fd。

So, for example, my second solution has to be rewritten for zsh as {my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(which works in bash too, but is awfully verbose).

因此,例如,我的第二个解决方案必须为 zsh 重写为{my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(它也适用于 bash,但非常冗长)。

On the other hand, you can take advantage of zsh's mysterious built-in implicit teeing to get a much shorter solution for zsh, which doesn't run tee at all:

另一方面,您可以利用 zsh 神秘的内置隐式 tee 为 zsh 获得更短的解决方案,它根本不运行 tee:

my_command >&1 >stdout.txt 2>&2 2>stderr.txt

(I wouldn't have guessed from the docs I found that the >&1and 2>&2are the thing that trigger zsh's implicit teeing; I found that out by trial-and-error.)

(我不会从我发现的文档中猜到>&12>&2是触发 zsh 隐式开球的东西;我通过反复试验发现了这一点。)

回答by gnud

Use the scriptcommand in your script (man 1 script)

script在脚本中使用命令(man 1 脚本)

Create a wrapper shellscript (2 lines) that sets up script() and then calls exit.

创建一个包装器 shellscript(2 行)来设置 script(),然后调用 exit。

Part 1: wrap.sh

第 1 部分:wrap.sh

#!/bin/sh
script -c './realscript.sh'
exit

Part 2: realscript.sh

第 2 部分:realscript.sh

#!/bin/sh
echo 'Output'

Result:

结果:

~: sh wrap.sh 
Script started, file is typescript
Output
Script done, file is typescript
~: cat typescript 
Script started on fr. 12. des. 2008 kl. 18.07 +0100
Output

Script done on fr. 12. des. 2008 kl. 18.07 +0100
~:

回答by denis

A year later, here's an old bash script for logging anything. For example,
teelog make ...logs to a generated log name (and see the trick for logging nested makes too.)

一年后,这是一个用于记录任何内容的旧 bash 脚本。例如,
teelog make ...记录到生成的日志名称(也请参阅记录嵌套makes的技巧。)

#!/bin/bash
me=teelog
Version="2008-10-9 oct denis-bz"

Help() {
cat <<!

    $me anycommand args ...

logs the output of "anycommand ..." as well as displaying it on the screen,
by running
    anycommand args ... 2>&1 | tee `day`-command-args.log

That is, stdout and stderr go to both the screen, and to a log file.
(The Unix "tee" command is named after "T" pipe fittings, 1 in -> 2 out;
see http://en.wikipedia.org/wiki/Tee_(command) ).

The default log file name is made up from "command" and all the "args":
    $me cmd -opt dir/file  logs to `day`-cmd--opt-file.log .
To log to xx.log instead, either export log=xx.log or
    $me log=xx.log cmd ...
If "logdir" is set, logs are put in that directory, which must exist.
An old xx.log is moved to /tmp/$USER-xx.log .

The log file has a header like
    # from: command args ...
    # run: date pwd etc.
to show what was run; see "From" in this file.

Called as "Log" (ln -s $me Log), Log anycommand ... logs to a file:
    command args ... > `day`-command-args.log
and tees stderr to both the log file and the terminal -- bash only.

Some commands that prompt for input from the console, such as a password,
don't prompt if they "| tee"; you can only type ahead, carefully.

To log all "make" s, including nested ones like
    cd dir1; $(MAKE)
    cd dir2; $(MAKE)
    ...
export MAKE="$me make"

!
  # See also: output logging in screen(1).
    exit 1
}


#-------------------------------------------------------------------------------
# bzutil.sh  denisbz may2008 --

day() {  # 30mar, 3mar
    /bin/date +%e%h  |  tr '[A-Z]' '[a-z]'  |  tr -d ' '
}

edate() {  # 19 May 2008 15:56
    echo `/bin/date "+%e %h %Y %H:%M"`
}

From() {  # header  # from: $*  # run: date pwd ...
    case `uname` in Darwin )
        mac=" mac `sw_vers -productVersion`"
    esac
    cut -c -200 <<!
${comment-#} from: $@
${comment-#} run: `edate`  in $PWD `uname -n` $mac `arch` 

!
    # mac $PWD is pwd -L not -P real
}

    # log name: day-args*.log, change this if you like --
logfilename() {
    log=`day`
    [[  == "sudo" ]]  &&  shift
    for arg
    do
        log="$log-${arg##*/}"  # basename
        (( ${#log} >= 100 ))  &&  break  # max len 100
    done
            # no blanks etc in logfilename please, tr them to "-"
    echo $logdir/` echo "$log".log  |  tr -C '.:+=[:alnum:]_\n' - `
}

#-------------------------------------------------------------------------------
case "" in
-v* | --v* )
    echo "
 program 2>&1 | tee > logfile
version: $Version" exit 1 ;; "" | -* ) Help esac # scan log= etc -- while [[ == [a-zA-Z_]*=* ]]; do export "" shift done : ${logdir=.} [[ -w $logdir ]] || { echo >&2 "error: $me: can't write in logdir $logdir" exit 1 } : ${log=` logfilename "$@" `} [[ -f $log ]] && /bin/mv "$log" "/tmp/$USER-${log##*/}" case ${0##*/} in # basename log | Log ) # both to log, stderr to caller's stderr too -- { From "$@" "$@" } > $log 2> >(tee /dev/stderr) # bash only # see http://wooledge.org:8000/BashFAQ 47, stderr to a pipe ;; * ) #------------------------------------------------------------------------------- { From "$@" # header: from ... date pwd etc. "$@" 2>&1 # run the cmd with stderr and stdout both to the log } | tee $log # mac tee buffers stdout ? esac

回答by tvanfosson

Use the tee program and dup stderr to stdout.

使用 tee 程序并将 stderr 复制到 stdout。

${APP_HOME}/.sh      2>&1 | tee -a ${APP_HOME}/.log

回答by JPLemme

I created a script called "RunScript.sh". The contents of this script is:

我创建了一个名为“RunScript.sh”的脚本。这个脚本的内容是:

./RunScript.sh ScriptToRun Param1 Param2 Param3 ...

I call it like this:

我这样称呼它:

##代码##

This works, but it requires the application's scripts to be run via an external script. It's a bit kludgy.

这有效,但它需要通过外部脚本运行应用程序的脚本。它有点笨拙。