是否有替代tee的方法来捕获正在执行的命令的标准输出和标准错误,并以与已处理命令相同的退出状态退出?
类似于以下内容:
eet -a some.log -- mycommand --foo --bar
其中“eet”是“tee”的虚构替代品:)(-a表示附加并--
分隔捕获的命令)。破解这样的命令应该不难,但也许它已经存在而我不知道?
这适用于 Bash:
(
set -o pipefail
mycommand --foo --bar | tee some.log
)
括号用于将 pipefail 的影响限制为仅一个命令。
pipefail
除非启用该选项 ,否则管道的返回状态是最后一个命令的退出状态。如果pipefail
启用,则管道的返回状态是最后一个(最右边)以非零状态退出的命令的值,或者如果所有命令都成功退出,则为零。
我在使用 Pipe & Tee 捕获退出代码时偶然发现了几个有趣的解决方案。
Bash 中有可用的 $PIPESTATUS 变量:
false | tee /dev/null
[ $PIPESTATUS -eq 0 ] || exit $PIPESTATUS
Perl 中“eet”的最简单原型可能如下所示:
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;
这是一个eet
. 适用于从 2.05b 到 4.0 的所有 Bash。
#!/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 左右引入的。)
这是我认为最好的纯 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
将在标准输出(文件描述符 1)上执行并打印其常规输出,然后一旦完成,echo
将在其标准输出上执行并打印command1
退出代码,但标准输出被重定向到文件描述符三。
在command1
运行时,它的标准输出被传送到 command2 (echo
的输出永远不会到达 command2 ,因为我们将它发送到文件描述符 3 而不是 1 ,这是管道读取的内容)。然后我们将command2
的输出重定向到文件描述符 4,这样它也不会出现在文件描述符 1 之外——因为当我们将echo
文件描述符 3 上的输出带回文件描述符 1 时,我们希望文件描述符 1 清除,以便命令替换(反引号)可以捕获它。
最后一点神奇之处在于,exec 4>&1
我们首先将其作为一个单独的命令执行——它将文件描述符 4 作为外部 shell 标准输出的副本打开。命令替换将从其内部命令的角度捕获标准输出中写入的任何内容 - 但是,由于command2
就命令替换而言,输出将发送到文件描述符 4,因此命令替换不会捕获它 - 但是,一旦它从命令替换中“退出”,它实际上仍然会进入脚本的整体文件描述符之一。
(exec 4>&1
必须是一个单独的命令才能与许多常见的 shell 一起工作。在某些 shell 中,如果你只是将它与变量赋值放在同一行,在替换的结束反引号之后,它就可以工作。)
(我在示例中使用复合命令({ ... }
),但子shell(( ... )
)也可以工作。子shell只会导致子进程的冗余分叉和等待,因为管道的每一侧和命令替换的内部通常已经暗示一个子进程的分叉和等待,我不知道有任何外壳被编码以识别它可以跳过其中一个分叉,因为它已经完成或即将执行另一个。)
你可以用一种不那么技术性和更有趣的方式来看待它,就好像命令的输出是相互跳跃的:command1
管道到command2
,然后echo
' 的输出跳过command2
这样command2
它就不会捕获它,然后command2
' 的输出跳跃一遍又一遍地执行命令替换,就像echo
及时降落以被替换捕获,以便它最终出现在变量中,并且command2
' 的输出继续到标准输出,就像在普通管道中一样。
另外,据我了解,在此命令的末尾,$?
仍将包含管道中第二个命令的返回码,因为变量赋值、命令替换和复合命令对内部命令的返回码都是有效透明的他们,所以返回状态command2
应该被传播出去。
需要注意的是,有可能command1
最终会使用文件描述符 3 或 4,或者command2
后面的任何命令将使用文件描述符 4,所以为了更卫生,我们会这样做:
exec 4>&1
exitstatus=`{ { command1 3>&-; echo $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-
命令从启动它们的进程继承文件描述符,因此整个第二行将继承文件描述符 4,而后面的复合命令3>&1
将继承文件描述符 3。因此4>&-
确保内部复合命令不会继承文件描述符 4,并3>&-
确保command1
不会继承文件描述符 3,因此 command1 获得了一个“更干净”、更标准的环境。您也可以将内部移动到4>&-
旁边3>&-
,但我想为什么不尽可能地限制它的范围。
几乎没有程序直接使用预先打开的文件描述符 3 和 4,因此您几乎不必担心它,但后者可能最好记住并用于通用情况。
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
{ mycommand --foo --bar 2>&1; ret=$?; } | tee -a some.log; (exit $ret)
假设 Bash 或 Z shell ( zsh
),
my_command >>my_log 2>&1
注意 将标准错误重定向和复制到标准输出的顺序很重要!
我没有意识到你也想在屏幕上看到输出。这当然会将所有输出定向到文件my_log。
#!/bin/sh
logfile="$1"
shift
exec 2>&1
exec "$@" | tee "$logfile"
希望这对你有用。