如果命令无法执行,我发现在反引号调用中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。
答案 0 :(得分:14)
您的语法是正确的,但在一种情况下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
的文件描述符为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
通常是perlfunc documentation on open
的包装。
现在说我们要为命令输出的每一行添加一个前缀。
$ 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
sed
等待waitpid
的退出状态
pipe
以向perl
fork
运行perl
dup2
让STDIN
从管道的读取端读取exec
sed
命令STDIN
的输入
dup2
将STDOUT
发送到第3步的管道写入端dup2
将STDERR
发送到STDOUT
的目的地exec
perl
命令exit
perl
waitpid
的退出状态
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
的首次调用为我们做了很多工作,如Using open
for IPC in perlipc中所述:
对于三个或更多个参数,如果MODE为
"|-"
,则文件名被解释为输出要通过管道输出的命令,如果MODE为"-|"
,则文件名被解释为命令管道输出给我们。在双参数(和单参数)形式中,应该用命令替换破折号("-"
)。有关此问题的更多示例,请参阅pp_backtick
。
它的输出是
$ ./simple-pipeline got: bye got: hi
上面的代码硬编码STDOUT
的重复,我们可以在下面看到。
$ ./simple-pipeline >/dev/null
要捕获另一个命令的输出,perl
设置相同的机制,您可以在Perl_my_popen
(pp_sys.c
)中看到,该a relevant comment调用perldiag(在util.c
)创建子流程并设置管道(fork
,pipe
,dup2
)。孩子做了一些管道并调用Perl_do_exec3
(在doio.c
中)来启动我们想要的输出命令。我们注意到{{3}}:
/* 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
pragma。 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
使新创建的孩子改为执行另一个程序。要捕获另一个命令的输出,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
不存在。孩子仍会在exec
上发出失败的STDERR
警告。由于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 非常良好做法时! - 这会启用exec
警告类别。要查看这些警告的完整列表,请在{{3}}文档中搜索字符串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警告,但这也意味着我必须非常小心地测试返回值。
答案 1 :(得分:7)
这里的问题是重定向是由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
答案 2 :(得分:1)
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重定向到文件时,将打印输出。这很奇怪,但是很有效。