在 Perl 中从 STDIN 捕获退出状态

Posted

技术标签:

【中文标题】在 Perl 中从 STDIN 捕获退出状态【英文标题】:Capturing exit status from STDIN in Perl 【发布时间】:2010-06-07 21:44:45 【问题描述】:

我有一个使用如下命令运行的 perl 脚本:

/path/to/binary/executable | /path/to/perl/script.pl

脚本对二进制文件的输出做了一些有用的事情,然后在 STDIN 用完时退出( 返回 undef)。这一切都很好,除非二进制文件以非零代码退出。从脚本的 POV 来看,它认为脚本刚刚结束,所以它清理并退出,代码为 0

有没有办法让 perl 脚本查看退出代码是什么?理想情况下,我希望这样的工作:

# close STDIN, and if there was an error, exit with that same error.
unless (close STDIN) 
   print "error closing STDIN: $! ($?)\n";
   exit $?;

但不幸的是,这似乎不起作用:

$ (date; sleep 3; date; exit 1) | /path/to/perl/script.pl /tmp/test.out
Mon Jun  7 14:43:49 PDT 2010
Mon Jun  7 14:43:52 PDT 2010
$ echo $?
0

有没有办法让它按照我的意思去做?

编辑添加:

perl 脚本正在实时处理二进制命令的输出,因此将其全部缓冲到一个文件中并不是一个可行的解决方案。但是,在脚本结束之前它不需要知道退出代码。

【问题讨论】:

【参考方案1】:

bash 环境变量$PIPESTATUS 是一个数组,其中包含最后一个命令管道的每个部分的状态。例如:

$ false | true; echo "PIPESTATUS: $PIPESTATUS[@];  ?: $?"
PIPESTATUS: 1 0;  ?: 0

所以听起来不像重构你的 perl 脚本,你只需要运行该管道命令的脚本来检查$PIPESTATUS。使用不带[@]$PIPESTATUS 可以得到数组第一个元素的值。

如果您需要检查初始可执行文件和 perl 脚本的状态,您需要先将 $PIPESTATUS 分配给另一个变量:

status=($PIPESTATUS[@])

然后你可以单独检查它们,比如

if (( $status[0] )); then echo "main reactor core breach!"; exit 1;
elif (( $status[1] )); then echo "perls poisoned by toxic spill!"; exit 2;
fi;

您必须通过临时变量执行此操作,因为下一条语句,即使它是 if 语句,也会在以下语句之前重置 $PIPESTATUS[@],即使它是 elif 语句,也可以检查它。

请注意,这些东西只适用于 bash 而不是原始的 bourne shell(通常是 sh,尽管许多系统将 /bin/sh 链接到 /bin/bash,因为它具有向后兼容性)。所以如果你把它放在一个shell脚本中,第一行应该是

#!/bin/bash

而不是#!/bin/sh

【讨论】:

从技术上讲,您应该使用echo "whatever" >&2; 将那些echo es 发送到stderr 不幸的是,我试图避免 BASHisms(试图让它在 dash 上工作,原因我无法控制)。在 bash 上,我目前正在使用“set -o pipefail”,它使整个管道返回第一个不成功命令的 RC。 嗯。我猜你需要使用一个临时文件,然后,类似于@DVK 的答案,但是在你从标准输入获得 EOF 之后,只需从 perl 中读取临时文件的内容。我曾考虑过使用 fifo,但这不起作用,因为 perl 在读取 fifo 之前不会从 stdin 看到 EOF,并且可能在输入完成之前不会检查 fifo,即 deadlock。虽然我猜你可以循环从标准输入和先进先出读取超时读取,直到你使用先进先出(然后从标准输入得到 EOF)。这有点复杂,无论如何你仍然必须制作先进先出,但它比临时文件更容易。【参考方案2】:

为了详细说明 Ether 的提议,这是 shell 变通方法:

bash-2.03$ TF=/tmp/rc_$$; (/bin/false; echo $?>$TF) | 
           perl5.8 -e 'print "OUT\n"'; test `cat $TF|tr -d "\012"` -eq 0 
OUT
bash-2.03$ echo $?
1
bash-2.03$ TF=/tmp/rc_$$; (/bin/true; echo $?>$TF) | 
           perl5.8 -e 'print "OUT\n"'; test `cat $TF|tr -d "\012"` -eq 0     
OUT
bash-2.03$ echo $?
0
bash-2.03$ 

缺点:

一般的丑陋

留下一堆 /tmp/rc_* 文件

丢失非零退出代码的 exact 值(在上面的示例中为 255)

您可以通过将 Perl 脚本轻微编辑为:

读入名为$ENVTF的文件的内容(使用File::Slurp::read_file()),说成my $rc chomp $rc; 取消链接$ENVTF exit $rc 然后你的命令行变成: TF=/tmp/rc_$$; (/bin/true; echo $?>$TF) | /your/perl/script

与 Ether 使用 system() 调用的脚本更改相比,这是一个侵入性较小的更改 - 您只需在脚本的最后添加 4 行(包括 exit);但是一旦你改变了脚本,我可能会建议你全力以赴,首先做 Ether 建议的改变。

【讨论】:

谢谢 - 额外的缺点(在这种情况下是一个关键的缺点)是在命令运行期间输出将不可用,只有在它完成后才可用。 @zigdon - 抱歉,没有看到您的最后一次编辑。你是对的 - 据我所知,子 shell 调用(括号)确实会缓冲命令的输出。【参考方案3】:

我看到两个选项:

你可以重写脚本,让它自己调用命令,这样它就可以检测到它的退出状态,并在它没有成功退出时采取不同的行动 您可以将命令的调用包装在 shell 脚本中,该脚本检查退出值,然后以不同的方式调用 Perl 脚本(与选项 1 基本相同,只是不需要更改Perl 脚本)。

但是,由于您是在命令退出之前从命令读取 Perl 脚本中的输入,因此您显然还没有返回码。您只能在命令完成后才能访问它,因此您需要同时将其输出缓冲到其他地方,例如文件:

use IPC::System::Simple qw(system $EXITVAL);
use File::Temp;

my $tempfile = File::Temp->new->filename;
system("/path/to/binary/executable > $tempfile");
if ($EXITVAL == 0)

     system("/path/to/perl/script.pl < $tempfile");

else

     die "oh noes!";

【讨论】:

+1 用于 Perl 修复,我还添加了一个详细说明纯 shell 解决方案的答案,以及一个 shell 解决方案与对脚本稍有“剧烈”变化的组合 :)【参考方案4】:

您只能获得您自己的子进程的退出状态。连接到您的 STDIN 的东西不是 perl 的子进程;这是贝壳的。很遗憾,你想要的都是不可能的。

【讨论】:

【参考方案5】:

不幸的是,bash 似乎抛弃了管道上的退出状态。运行 "sleep 3 | echo hi" 会在 sleep 完成之前启动 echo,因此它绝对没有机会捕获第一个命令的退出状态。

您可以(理论上)通过将 bash 命令更改为命令列表来运行它——bash 会将值保存在 $? (就像 Perl 一样),但是您必须以某种方式将其传递给 Perl 脚本,这意味着您的 Perl 脚本将需要在(例如)命令行上接受前一个程序的退出状态。

或者,您可以只重写 Perl 脚本来运行命令,并捕获退出状态,或者将整个内容包装在另一个脚本中。

【讨论】:

“bash 抛弃了管道上的退出状态”:这不是真的——请看我的回答。

以上是关于在 Perl 中从 STDIN 捕获退出状态的主要内容,如果未能解决你的问题,请参考以下文章

从最后一个管道(stdin)获取退出代码

在 Perl DBI 中从 PostgreSQL 捕获 NOTICE

在c ++中捕获shell脚本退出状态[重复]

从 R 中的系统调用捕获退出状态和输出

python:运行一个超时的进程并捕获stdout、stderr和退出状态[重复]

如何在 Perl 中捕获输出和退出代码时执行外部脚本?