如何同时将STDOUT和STDERR转到终端和日志文件?


104

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

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

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

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

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


对于其他读者:类似的问题:stackoverflow.com/questions/692000/…–
pevik

1
我很烦恼除@JasonSydes之外的每个人(包括我!)出轨并回答了另一个问题。正如我评论的那样,杰森的答案是不可靠的。我很乐意为您提出的问题提供一个真正可靠的答案(并在您的EDIT中强调)。
唐·哈奇

哦,等等,我拿回去了。@PaulTromblin接受的答案确实可以回答。我对它的阅读不够深入。
唐·哈奇

Answers:


167

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

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

要么

./a.out |& tee output

在csh中,有一个名为“脚本”的内置命令,它将捕获所有进入屏幕的文件。您可以通过键入“脚本”来启动它,然后执行您要捕获的所有内容,然后按Control-D关闭脚本文件。我不知道sh / bash / ksh的等效项。

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

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

4
我不知道您可以在shell脚本中将命令括起来。有趣。
杰米

1
我也很欣赏Bracket快捷键!由于某种原因,2>&1 | tee -a filename并不是将stderr从脚本中保存到文件中,但是当我复制命令并将其粘贴到终端中时,它工作正常!括号把戏很好用。
Ed Brannin 09年

8
请注意,由于tee将所有内容打印到stdout,因此stdout和stderr之间的区别将丢失。
Flimm 2015年

2
仅供参考:大多数脚本中都提供“脚本”命令(它是util-linux软件包的一部分)
SamWN

2
@Flimm,是否有其他方法可以保留stdout和stderr之间的区别?
加百利

20

接近五年后...

我相信这是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。已从此处注释固定。


3
请注意,由于tee将所有内容打印到stdout,因此stdout和stderr之间的区别将丢失。
Flimm 2015年

@Flimm stderr可以重定向到其他tee过程,该过程可以再次重定向到stderr。
jarno

@Flimm,我在这里写下了jarno的建议:stackoverflow.com/a/53051506/1054322
MatrixManAtYrService

1
与迄今为止提出的大多数其他解决方案一样,该解决方案也容易出现竞争。也就是说,当当前脚本完成并返回到用户提示或更高级别的调用脚本时,在后台运行的tee仍将运行,并且可能会将最后几行显示在屏幕上并向日志文件延迟(即提示后到达屏幕,并在日志文件完成后到达日志文件)。
唐·哈奇

1
但是,这是迄今为止提出的唯一可以实际解决该问题的答案!
唐·哈奇

9

模式

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

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

  • 在zsh中,直到tees完成后才继续执行下一条语句。

  • 在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进程都已启动,它们的标准输入已分配给文件描述符。因为它们包含在进程替换中,所以这些文件描述符的路径在调用命令中被替换,因此现在看起来像这样:

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的stdout 读取的第一个过程将该stdout的副本写回到调用方,因为那样tee做。它的输出不会重定向,因此它们可以不变地返回给调用方

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

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

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


这看起来真的很有用,也是我想要的。不过,我不确定如何在Windows Batch脚本中复制括号的使用(如第一行所示)。(tee在相关系统上可用。)我得到的错误是“该进程无法访问文件,因为该文件正在被另一个进程使用。”
Agi Hammerthief

与迄今为止提出的大多数其他解决方案一样,该解决方案也容易出现竞争。也就是说,当当前脚本完成并返回到用户提示或更高级别的调用脚本时,在后台运行的tee仍将运行,并且可能会将最后几行显示在屏幕上并向日志文件延迟(即提示后到达屏幕,并在日志文件完成后到达日志文件)。
唐·哈奇

2
@DonHatch您可以提出解决此问题的解决方案吗?
pylipp

我也会对使比赛显而易见的测试用例感兴趣。并不是我对此表示怀疑,但是很难避免它,因为我还没有看到它的发生。
MatrixManAtYrService

@pylipp我没有解决办法。我会对一个非常感兴趣。
唐·哈奇

4

要将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

2
请注意,由于tee将所有内容打印到stdout,因此stdout和stderr之间的区别将丢失。
Flimm 2015年

4

编辑:我看到我出轨了,并最终回答了一个与被问到的问题不同的问题。真正问题的答案在保罗·汤姆林的答案的底部。(如果您出于某种原因想要增强该解决方案以分别重定向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. 2>&3-zsh无法理解该语法;必须重写为2>&3 3>&-
  2. 在zsh中(与其他Shell不同),如果您重定向已经打开的文件描述符,则在某些情况下(我不完全了解它的决定),它会执行内置的类似tee的行为。为避免这种情况,您必须在重定向每个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神秘的内置隐式teeing来获得zsh的更短解决方案,而后者根本不会运行tee:

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

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


我用bash玩了一下,效果很好。只是对习惯假设兼容性的zsh用户(如我自己)发出警告,它在那儿的行为有所不同:gist.github.com/MatrixManAtYrService/…–
MatrixManAtYrService

@MatrixManAtYrService我相信我对zsh的情况有所了解,事实证明zsh中有一个更整洁的解决方案。参见我的编辑“关于zsh的脚注”。
唐·哈奇

感谢您详细解释解决方案。您还知道my_function在嵌套的stdout / stderr过滤中使用函数()时如何检索返回代码吗?我做到了,{ { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filter但是创建文件作为失败指示很奇怪...
pylipp

@pylipp我不会用完。您可能会把它作为一个单独的问题问(也许使用更简单的管道)。
唐·哈奇

2

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

创建一个用于设置script()的包装器shellscript(两行),然后调用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
~:


1

我创建了一个名为“ 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 ...

这可行,但是需要通过外部脚本来运行应用程序的脚本。这有点糊涂。


9
您将丢失包含$ 1 $ 2 $ 3 ...的包含空格的参数分组,您应该使用(w / quotes): “ $ @”
NVRAM

1

一年后,这是一个用于记录任何内容的旧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`
    [[ $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

我知道添加评论已为时已晚,但是我只想对这个脚本表示感谢。非常有用且有据可查!
stephenmm'5

谢谢@stephenmm; 它永远不会来不及说“有用”或“有待改进”。
丹尼斯2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.