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

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

我希望 STDOUT 和 STDERR 都重定向到终端(这样用户就可以看到脚本正在工作,也可以看到是否有问题)。我还希望这两个流都重定向到一个日志文件。

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

完美的解决方案是将一行代码合并到任何脚本的开头,这些脚本同时向终端和日志文件发送两个流。

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

108585 次浏览

使用“ tee”重定向到文件和屏幕。根据所使用的 shell,首先必须使用

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

或者

./a.out |& tee output

在 csh 中,有一个称为“ script”的内置命令,它将把到达屏幕的所有内容都捕获到一个文件中。首先输入“ script”,然后执行您想要捕获的任何操作,然后点击 control-D 关闭脚本文件。我不知道 sh/bash/ksh 的对应词。

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

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

使用 tee 程序并将 stderr 转换为 stdout。

 program 2>&1 | tee > logfile

我创建了一个名为“ RunScript.sh”的脚本:

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

我这样称呼它:

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

这是可行的,但是它要求应用程序的脚本通过外部脚本运行。

要将 stderr 重定向到 stdout,请在命令中附加以下内容: 2>&1 对于输出到终端和登录到文件,您应该使用 tee

两者放在一起就像这样:

 mycommand 2>&1 | tee mylogfile.log

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

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

最终会变成

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

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

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

第1部分: wrap.sh

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

第2部分: realscript.sh

#!/bin/sh
echo 'Output'

结果:

~: 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
~:

一年后,这里有一个旧的 bash 脚本,用于记录任何东西,
teelog make ...记录到一个生成的日志名称(并参见记录嵌套的 make的技巧)

#!/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`
[[ $1 == "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 "$1" in
-v* | --v* )
echo "$0 version: $Version"
exit 1 ;;
"" | -* )
Help
esac


# scan log= etc --
while [[ $1 == [a-zA-Z_]*=* ]]; do
export "$1"
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

将近五年之后。

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

下面是一个可以添加到 Bash 脚本顶部的代码行:

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

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

#!/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

(注意: 这只适用于 Bash,没有适用于/bin/sh。)

改编自 给你; 据我所知,原版没有在日志文件中捕获 STDERR。修正了来自 给你的音符。

模式

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

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

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

  • 在 bash 中,您可能会发现输出的最后几行显示 之后,不管接下来是什么语句。

无论哪种情况,正确的位置都是正确的。


解释

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

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


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

这里有一个环节:

$ foo=$(./example)
err


$ echo $foo
out


$ cat stdout.txt
out


$ cat stderr.txt
err

它是这样运作的:

  1. 启动两个 tee进程,它们的 stdin 被分配给文件描述符。因为它们被包含在 工艺替代品工艺替代品中,这些文件描述符的路径在调用命令中被替换,所以现在看起来像这样:

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

  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 读取的)。所以当它写入它的 stdout 时,这些位就会转到调用者的 stderr。

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

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

编辑: 我发现我脱轨了,最后回答了一个和问题不同的问题。真正问题的答案在 Paul Tomblin 的答案的底部。(如果出于某种原因希望增强该解决方案以分别重定向 stdout 和 stderr,可以使用我在这里描述的技术。)


我一直想要一个能保留 stdout 和 stderr 区别的答案。 不幸的是,到目前为止所有的答案都保持了这种区别 有种族倾向: 正如我在评论中指出的那样,它们可能使程序看到不完整的输入。

我想我终于找到了一个能保持这种区别的答案, 不是种族主义者,也不是非常精确。

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

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

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

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

现在剩下的就很简单了: 我们可以在开始时添加一个 stdout 过滤器:

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

或者在结尾:

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

为了让自己相信上述两个命令都能正常工作,我使用了以下命令:

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;}'

产出为:

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

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

关于 zsh 的脚注 :

上面的解决方案在 bash 中可以工作(也许还有其他一些 shell,我不确定) ,但是在 zsh 中不能工作。它在 zsh 中失败有两个原因:

  1. Zsh 不能理解语法 2>&3-; 必须重写它 作为 2>&3 3>&-
  2. 在 zsh 中(与其他 shell 不同) ,如果重定向文件描述符 它已经打开了,在某些情况下(我不完全理解它是如何决定的) ,它实现了一个内置的类似 T 恤的行为。为了避免这种情况,您必须在以下命令之前关闭每个 fd 改变它的方向。

因此,例如,我的第二个解决方案必须为 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 中也可以工作,但是非常冗长)。

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

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

(我不会从文档中猜测,是 >&12>&2触发了 zsh 的隐性发球; 我是通过反复试验才发现这一点的。)

下面是一个通过重定向为 bash 工作的解决方案,它结合了“ kvantour,MatrixManAtYrService”和“ Jason Sydes”的解决方案:

#!/bin/bash
exec 1> >(tee x.log) 2> >(tee x.err >&2)


echo "test for log"
echo "test for err" 1>&2

将上面的脚本保存为 x.sh。跑完之后。/x.sh,x.log 只包含 stdout,而 x.err 只包含 stderr。

这个问题似乎还没有得到优雅的解决。 每次我搜索“如何同时输出到 stdout 和 stderr”,谷歌指引我到这篇文章。

今天,我终于找到了一个简单有效的方法来解决几乎所有这些需求。

其基本思想是 tee 命令,它可以同时打印到多个输出,而 Linux 特定的/proc/self/fd/{1,2,... }表示 stdout、 stderr..。

  • 将 stdin 打印到 stdout 和 stderr
tee /proc/self/fd/2
  • 将 stdin 打印到 stdout 和 stderr,然后输出文件
tee /proc/self/fd/2 file

希望这对你有帮助。