"tee" 和退出状态

21

是否有一种替代tee的方法,可以捕获正在执行的命令的标准输出标准错误,并以与处理后的命令相同的退出状态退出?

就像以下示例一样:

eet -a some.log -- mycommand --foo --bar

这里的“eet”是对“tee”的想象替代品 :)(-a表示追加,--分隔捕获的命令)。攻击这样的命令不应该很困难,但也许它已经存在而我没有意识到?


3
我认为这里真正的问题是:如何同时输出并捕获退出状态。如果是这样的话,请参考 bash: tee output AND capture exit status 可能是一个重复的问题。 - Lesmana
9个回答

35

这个适用于Bash:

(
  set -o pipefail
  mycommand --foo --bar | tee some.log
)

括号的作用是将pipefail的影响限制在单个命令上。

bash(1)手册中可知:

如果未启用pipefail选项,则管道的返回状态为最后一个命令的退出状态。如果启用了pipefail选项,则管道的返回状态是最后一个(最右边的)非零状态命令的值,或者如果所有命令都成功退出则为零。

11

我在PerlMonks上发现了一些有趣的解决方案,可以使用管道和tee命令捕获退出代码。

  1. There is the $PIPESTATUS variable available in Bash:

    false | tee /dev/null
    [ $PIPESTATUS -eq 0 ] || exit $PIPESTATUS
    
  2. And the simplest prototype of "eet" in Perl may look as follows:

    open MAKE, "command 2>&1 |" or die;
    open (LOGFILE, ">>some.log") or die;
    while (<MAKE>) { 
        print LOGFILE $_; 
        print 
    }
    close MAKE; # To get $?
    my $exit = $? >> 8;
    close LOGFILE;
    

5

这是一个eet。它可以在我手头上能找到的所有Bash版本中使用,从2.05b到4.0。

#!/bin/bash
tee_args=()
while [[ $# > 0 && $1 != -- ]]; do
    tee_args=("${tee_args[@]}" "$1")
    shift
done
shift
# now ${tee_args[*]} has the arguments before --,
# and $* has the arguments after --

# redirect standard out through a pipe to tee
exec | tee "${tee_args[@]}"

# do the *real* exec of the desired program
exec "$@"

(pipefail$PIPESTATUS很不错,但我记得它们是在3.1或附近引入的。)


很奇怪 - 但对我不起作用:jirka@debian:/monitor$ exec | wc -c \ 0 \ jirka@debian:/monitor$ exec echo a \ a(\表示换行) - jpalecek

3
这是我认为最好的纯Bourne Shell解决方案,可以用作基础来构建你的“eet”:
# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; echo $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

我认为最好从内部向外解释这个过程 - command1会执行并将其常规输出打印在stdout(文件描述符1)上,然后一旦它完成,echo将执行并在其stdout上打印command1的退出代码,但是该stdout被重定向到文件描述符3。
command1正在运行时,它的stdout正在被管道传输到command2(因为我们将其发送到文件描述符3而不是1,这是管道读取的内容,所以echo的输出永远不会传递给command2)。然后,我们将command2的输出重定向到文件描述符4,以使其也不进入文件描述符1,因为我们希望文件描述符1清除,以便我们将文件描述符三中的echo输出带回到文件描述符1中,以便命令替换(反引号)可以捕获它。
最后的魔法是我们首先作为一个单独的命令exec 4>&1 - 它将文件描述符四打开为外部shell的stdout的副本。命令替换将捕获从其内部命令的角度写入标准输出的任何内容 - 但是,由于command2的输出就像命令替换所关心的一样进入文件描述符四,因此命令替换不会捕获它 - 但是,一旦它“出”了命令替换,它实际上仍然会进入脚本的整体文件描述符1。
exec 4>&1必须是单独的命令才能与许多常见的shell一起使用。在某些shell中,如果您将其放在变量赋值的同一行上,在替换的关闭反引号之后,它也可以工作。)
(我在示例中使用复合命令({ ... }),但子shell(( ... ))也可以工作。子shell只会导致冗余的fork和等待子进程,因为管道的每一侧和命令替换的内部通常都意味着fork和等待子进程,并且我不知道任何编码的shell能够识别它可以跳过其中一个fork,因为它已经完成或即将完成另一个fork。)
您可以以更少技术性和更有趣的方式来看待它,好像命令的输出正在相互追逐: command1传输到command2,然后echo的输出跳过command2,以便command2不会捕获它,然后command2的输出跳过并从命令替换中出来,正好在echo及时到达以被替换所捕获,以便最终出现在变量中,command2的输出按照正常管道的方式继续前进。
此外,据我所知,在此命令结束时,$? 仍将包含管道中第二个命令的返回代码,因为变量赋值、命令替换和复合命令对它们内部命令的返回代码都是透明的,因此 command2 的返回状态应该被传播出去。
需要注意的是,command1 可能会在某个时候使用文件描述符三或四,或者 command2 或后续的任何命令会使用文件描述符四,为了更加卫生,我们应该这样做:
exec 4>&1
exitstatus=`{ { command1 3>&-; echo $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

命令会继承启动它们的进程的文件描述符,因此整个第二行将继承文件描述符四,而后面跟着的复合命令 3>&1 将继承文件描述符三。所以 4>&- 确保内部复合命令不会继承文件描述符四,3>&- 则确保 command1 不会继承文件描述符三,因此 command1 获得了一个更 "干净"、更标准的环境。你也可以将内部的 4>&- 移到 3>&- 旁边,但我认为尽可能限制其范围比较好。

几乎没有程序直接使用预打开的文件描述符三和四,因此你几乎不用担心它们,但后者可能是最好记住并用于通用情况的。


2
{ mycommand --foo --bar 2>&1; ret=$?; } | tee -a some.log; (exit $ret)

4
至少在bash中,$ret在{list}之外不可用,因此这样做是行不通的。 - Steve Madsen
我喜欢这个解决方案的一个变体,其中$?的值被写入文件:{ mycommand --foo --bar; echo $? > exit-code; } | tee some.log; ret=$(cat exit-code)。是的,写入临时文件有些复杂,但它符合POSIX标准,不像pipefail是bashism,并且在某些情况下也无法正常工作。 - ctrueden

1

KornShell全部在一行中:

foo; RET_VAL=$?; if test ${RET_VAL} != 0;then echo $RET_VAL; echo Error occurred!>/tmp/out.err;exit 2;fi |tee >>/tmp/out.err ; if test ${RET_VAL} != 0;then exit $RET_VAL;fi

0

我也在寻找一个适用于AppleScript中的do shell script的一行代码,它总是使用/bin/sh(由zsh模拟)。这个版本是我找到的唯一一个运行良好的。

mycommand 2>&1 | tee -a output.log; exit ${PIPESTATUS[0]}

或者在 AppleScript 中

set theResult to do shell script "mycommand 2>&1 | tee -a " & quoted form of logFilePath & "; exit ${PIPESTATUS[0]}"

-2
#!/bin/sh
logfile="$1"
shift
exec 2>&1
exec "$@" | tee "$logfile"

希望这对你有用。

使用参数 "foo false" 运行此程序 -- 应该返回退出码 1 (来自 false),但我得到了 0 (可能来自 tee)。 - Steve Madsen
我的错。必须用老式的管道方式来做。PIPE=/tmp/$$.pipe; mkfifo "$PIPE"; logfile="$1"; shift; tee "$logfile" <"$PIPE" &; "$@" 2>&1 >"$PIPE"; status=$?; rm "$PIPE"; exit $status - Marko Teiste
你可以编辑(更改)你的回答。(但是不要包含“编辑:”,“更新:”或类似的内容 - 回答应该看起来像是今天写的。) - Peter Mortensen

-2
假设使用 Bash 或 Z shell(zsh),
my_command >>my_log 2>&1

注意:重定向和将标准错误复制到标准输出的顺序很重要!

我没有意识到你想在屏幕上看到输出。这当然会将所有输出都定向到文件my_log中。


2
这会将所有输出发送到my_log,但与tee不同的是,它不会在控制台上显示。 - Steve Madsen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接