[英]STDERR redirection to STDOUT lost if backticks can't exec
如果命令执行失败,我在反引号调用中发现STDERR
重定向可能会丢失。 我对我所看到的行为感到困惑。
$ perl -e 'use strict; use warnings; my $out=`DNE`; print $out' Can't exec "DNE": No such file or directory at -e line 1. Use of uninitialized value in print at -e line 1. $ perl -e 'use strict; use warnings; my $out=`DNE 2>&1`; print $out' Use of uninitialized value in print at -e line 1. $ perl -e 'use strict; use warnings; my $out=`echo 123; DNE 2>&1`; print $out' 123 sh: DNE: command not found
我的语法不正确吗?
我在Linux上使用Perl 5.8.5。
您的语法是正确的,但在一种情况下perl
正在删除错误消息。
通常,请考虑在初始化期间测试您的系统是否具有所需的命令,如果缺少则请尽早失败。
my $foopath = "/usr/bin/foo";
die "$0: $foopath is not executable" unless -x $foopath;
# later ...
my $output = `$foopath 2>&1`;
die "$0: $foopath exited $?" if $?;
要完全理解输出的差异,有必要了解Unix系统编程的细节。 继续阅读。
考虑一个简单的perl
调用。
perl -e 'print "hi\n"; warn "bye\n"'
它的输出是
hi bye
请注意, print
的输出转到标准输出STDOUT
,并warn
写入标准错误STDERR
。 从终端运行时,两者都出现在终端上,但我们可以将它们发送到不同的地方。 例如
$ perl -e 'print "hi\n"; warn "bye\n"' >/dev/null bye
空设备或/dev/null
会丢弃发送给它的任何输出,因此在上面的命令中,“hi”消失。 上面的命令是简写
$ perl -e 'print "hi\n"; warn "bye\n"' 1>/dev/null bye
也就是说,1是STDOUT
文件描述符。 相反,扔掉“再见”,跑
$ perl -e 'print "hi\n"; warn "bye\n"' 2>/dev/null hi
如您所见,2是STDERR
文件描述符。 (为完整STDIN
, STDIN
文件描述符为0.)
在Bourne shell及其衍生产品中,我们还可以将STDOUT
和STDERR
合并为2>&1
。 将其读作“make file descriptor 2的输出与文件描述符1的相同位置”。
$ perl -e 'print "hi\n"; warn "bye\n"' 2>&1 hi bye
终端输出不突出显示区别,但额外的重定向显示正在发生的事情。 我们可以通过跑步来丢弃它们
$ perl -e 'print "hi\n"; warn "bye\n"' >/dev/null 2>&1
订单与这个shell系列有关,它以从左到右的顺序处理重定向,因此转换两个产量
$ perl -e 'print "hi\n"; warn "bye\n"' 2>&1 >/dev/null bye
起初这可能会令人惊讶。 shell首先处理2>&1
,这意味着将STDERR
发送到与STDOUT
相同的目的地 - 它已经是:终端! 然后它处理>/dev/null
并将STDOUT
重定向到空设备。
这种文件描述符的重复是通过调用dup2
,它通常是fcntl
的包装器。
现在说我们要为命令输出的每一行添加一个前缀。
$ perl -e 'print "hi\n"; warn "bye\n"' | sed -e 's/^/got: /' bye got: hi
顺序不同,但请记住STDERR
和STDOUT
是不同的流。 另请注意,只有“hi”才能获得前缀。 要获得两条线,它们都必须出现在STDOUT
。
$ perl -e 'print "hi\n"; warn "bye\n"' 2>&1 | sed -e 's/^/got: /' got: bye got: hi
要构造管道,shell使用fork
创建子进程,使用dup2
执行重定向,并通过在适当的进程中调用exec
来启动管道的每个阶段。 对于上面的管道,过程类似于
fork
一个进程来运行sed
waitpid
等待sed
退出状态
pipe
以将输入提供给perl
fork
一个进程运行perl
dup2
使STDIN
从管道的读取端读取 exec
sed
命令 STDIN
输入
dup2
将STDOUT
从步骤3发送到管道的写端 dup2
将STDERR
发送到STDOUT
的目的地 exec
perl
命令 exit
waitpid
收获perl
的退出状态 exit
$?
使用waitpid
的返回值 请注意,子进程是按从右到左的顺序创建的。 这是因为Bourne系列中的shell将管道的退出状态定义为最后一个进程的退出状态。
您可以使用下面的代码在Perl中构建上述管道。
#! /usr/bin/env perl
use strict;
use warnings;
my $pid = open my $fh, "-|";
die "$0: fork: $!" unless defined $pid;
if ($pid) {
while (<$fh>) {
s/^/got: /;
print;
}
}
else {
open STDERR, ">&=", \*STDOUT or print "$0: dup: $!";
exec "perl", "-e", q[print "hi\n"; warn "bye\n"]
or die "$0: exec: $!";
}
第一次open
对我们来说做了很多工作,如open
上的perlfunc文档中所述:
对于三个或更多参数,如果MODE为
"|-"
,则文件名被解释为输出要通过管道输出的命令,如果MODE为"-|"
,文件名被解释为管道输出给我们的命令。 在双参数(和单参数)形式中,应该用命令替换破折号("-"
)。 有关此问题的更多示例,请参阅在perlipc中使用open
for IPC 。
它的输出是
$ ./simple-pipeline got: bye got: hi
上面的代码硬编码了STDOUT
的复制,我们可以在下面看到。
$ ./simple-pipeline >/dev/null
为了捕获另一个命令的输出, perl
设置了相同的机制,你可以在pp_backtick
(在pp_sys.c
)看到它,它调用Perl_my_popen
(在util.c
)来创建子进程并设置管道( fork
, pipe
, dup2
)。 孩子做了一些管道并调用Perl_do_exec3
(在doio.c
)来启动我们想要的输出命令。 我们注意到相关评论 :
/* handle the 2>&1 construct at the end */
该实现识别序列2>&1
,复制STDOUT
,并从命令中删除重定向以传递给shell。
if (*s == '>' && s[1] == '&' && s[2] == '1'
&& s > cmd + 1 && s[-1] == '2' && isSPACE(s[-2])
&& (!s[3] || isSPACE(s[3])))
{
const char *t = s + 3;
while (*t && isSPACE(*t))
++t;
if (!*t && (PerlLIO_dup2(1,2) != -1)) {
s[-2] = '\0';
break;
}
}
后来我们看到了
PerlProc_execl(PL_sh_path, "sh", "-c", cmd, (char *)NULL);
PERL_FPU_POST_EXEC
S_exec_failed(aTHX_ PL_sh_path, fd, do_report);
在S_exec_failed
内部,我们发现
if (ckWARN(WARN_EXEC))
Perl_warner(aTHX_ packWARN(WARN_EXEC), "Can't exec \"%s\": %s",
cmd, Strerror(e));
这是您在问题中提出的警告之一。
让我们详细了解perl
如何处理您问题中的命令。
$ perl -e 'use strict; use warnings; my $out=`DNE`; print $out' Can't exec "DNE": No such file or directory at -e line 1. Use of uninitialized value in print at -e line 1.
这里没有惊喜。
一个微妙的细节是很重要的理解。 只有在要执行的命令的条件为真时,上面的代码才能处理2>&1
内部运行:
if (*s != ' ' && !isALPHA(*s) &&
strchr("$&*(){}[]'\";\\|?<>~`\n",*s)) {
这是一个优化。 如果反引号中的命令包含上面的shell元字符,则perl
必须将其移交给shell。 但是如果没有shell元字符存在, perl
可以直接exec
命令 - 节省fork
和shell启动成本。
不存在的命令DNE
包含shell元字符,因此perl
完成所有工作。 生成exec-category警告是因为命令失败并且您启用了warnings
编译指示。 perlop文档告诉我们,当命令失败时,反引号或qx//
在标量上下文中返回undef
,这就是为什么你得到关于打印$out
的未定义值的警告。
$ perl -e 'use strict; use warnings; my $out=`DNE 2>&1`; print $out' Use of uninitialized value in print at -e line 1.
失败的exec
警告在哪里?
请记住创建运行另一个命令的子进程的基本步骤:
fork
来创建一个几乎相同的子进程。 dup2
将STDOUT
连接到管道的写入端。 exec
中, exec
使新创建的子进程执行另一个程序。 为了捕获另一个命令的输出, perl
完成了这些步骤。 在准备尝试运行DNE 2>&1
, perl
分叉一个孩子并且在子进程中导致STDERR
与STDOUT
重复,但是还有另一个副作用。
if (!*t && (PerlLIO_dup2(1,2) != -1)) {
s[-2] = '\0';
break;
}
如果2>&1
在命令的末尾并且dup2
成功,则perl
在重定向之前写入NUL字节。 这具有从命令中删除它的效果, 例如 , DNE 2>&1
变为DNE
! 现在,在命令中没有shell元字符的情况下, 子进程中的 perl
认为自己是'Self,我们可以直接exec
这个命令'。
对exec
的调用失败,因为DNE
不存在。 孩子仍然在STDERR
上发出失败的exec
警告。 由于dup2
将STDERR
指向与STDOUT
相同的位置,因此它不会转到终端:管道的写入端返回到父节点。
父进程检测到子进程异常退出,并忽略管道内容,因为命令执行失败的结果记录为undef
。
$ perl -e 'use strict; use warnings; my $out=`echo 123; DNE 2>&1`; print $out' 123 sh: DNE: command not found
在这里,我们看到不存在DNE
的不同诊断。 遇到的第一个shell元字符是;
,所以perl
将命令不变地移交给shell执行。 echo
正常完成,然后DNE
在shell中失败, shell的STDOUT
和STDERR
返回到父进程。 从perl
的角度来看,shell执行得很好,所以没有什么值得警告的。
当您启用warnings
pragma-a Very Good Practice! - 这将启用exec
警告类别。 要查看这些警告的完整列表,请在perldiag文档中搜索字符串W exec
。
观察差异。
$ perl -Mstrict -Mwarnings -e 'my $out=`DNE`; print $out' Can't exec "DNE": No such file or directory at -e line 1. Use of uninitialized value $out in print at -e line 1. $ perl -Mstrict -Mwarnings -M-warnings=exec -e 'my $out=`DNE`; print $out' Use of uninitialized value $out in print at -e line 1.
后者的调用相当于
use strict;
use warnings;
no warnings 'exec';
my $out = `DNE`;
print defined($out) ? $out : "command failed\n";
我喜欢在exec
,管道open
等问题时格式化我自己的错误消息。 这意味着我通常禁用exec警告,但这也意味着我必须非常小心地测试返回值。
这里的问题是重定向是由shell完成的。 并且你的```命令不是通过shell运行的 - Perl尝试使用$ PATH查找DNE程序,但它失败了。
如果你需要捕获stdout和stderr,你可以采用多种方式,但我认为最安全的是使用IPC :: Open3或IPC :: Run 。
如果你感觉很冒险,你可以尝试做这样的事情,但请记住这是个坏主意:
$ perl -e 'use strict; use warnings; my $o=`sh -c "DNE 2>&1"`; print $o'
sh: 1: DNE: not found
my $op =`dne 2>&1;`;
这有效。 注意分号;
在重定向结束时。
或者你可以使用下面的代码。
#!/usr/bin/perl
use strict;
use warnings;
my $op=`dne 2>&1 1>output.txt`;
print $op;
输出:
sh: dne: command not found
虽然,为什么在dne 2>&1
情况下不打印STDOUT仍然是我不知道的。
但是当您使用STDOUT重定向到文件时,将打印输出。 这很奇怪,但是很有效。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.