您的语法是正确的,但在一种情况下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 系统编程的细节。继续阅读。
Unix 文件描述符
考虑一个简单的perl
调用。
perl -e '打印 "hi\n"; 警告“再见\n”'
它的输出是
你好
再见
请注意,输出print
到STDOUT
标准输出,并warn
写入STDERR
标准错误。从终端运行时,两者都会出现在终端上,但我们可以将它们发送到不同的地方。例如
$ perl -e '打印 "hi\n"; 警告“再见\n”' >/dev/null
再见
空设备或/dev/null
丢弃发送给它的任何输出,因此在上面的命令中,“hi”消失了。上面的命令是简写
$ perl -e '打印 "hi\n"; 警告“再见\n”' 1>/dev/null
再见
也就是说,1 是 的文件描述符STDOUT
。要扔掉“再见”,请运行
$ perl -e '打印 "hi\n"; 警告“再见\n”' 2>/dev/null
你好
如您所见, 2 是STDERR
. (为完整起见,文件描述符为STDIN
0。)
在 Bourne shell 及其派生类中,我们还可以将STDOUT
和STDERR
与合并2>&1
。将其读作“使文件描述符 2 的输出与文件描述符 1 的输出到相同的位置。”</p>
$ perl -e '打印 "hi\n"; 警告“再见\n”' 2>&1
你好
再见
终端输出没有突出区别,但额外的重定向显示正在发生的事情。我们可以通过运行丢弃两者
$ perl -e '打印 "hi\n"; 警告“再见\n”' >/dev/null 2>&1
顺序对于这个以从左到右的顺序处理重定向的 shell 系列很重要,因此转置两个产生
$ perl -e '打印 "hi\n"; 警告“再见\n”' 2>&1 >/dev/null
再见
起初这可能令人惊讶。shell首先处理2>&1
这意味着发送STDERR
到与它已经是相同的目的地STDOUT
:终端!然后它处理>/dev/null
并重定向STDOUT
到空设备。
这种文件描述符的重复是通过调用dup2
来实现的,这通常是fcntl
.
重定向和管道
现在假设我们要为命令输出的每一行添加一个前缀。
$ perl -e '打印 "hi\n"; 警告“再见\n”' | sed -e 's/^/得到:/'
再见
得到:你好
顺序不同,但请记住,STDERR
它们STDOUT
是不同的流。另请注意,只有“hi”有前缀。要获得两条线,它们都必须出现在STDOUT
.
$ perl -e '打印 "hi\n"; 警告“再见\n”' 2>&1 | sed -e 's/^/得到:/'
得到:再见
得到:你好
为了构建管道,shell 使用 来创建子进程fork
,使用 来执行重定向dup2
,并通过调用exec
适当的进程来启动管道的每个阶段。对于上面的管道,过程类似于
- shell:
fork
要运行的进程sed
- shell:等待退出
sed
状态waitpid
- sed:创建一个
pipe
将输入提供给perl
- sed:
fork
要运行的进程perl
- sed:
dup2
从STDIN
管道的读取端读取
- sed
exec
:sed
命令
- sed:等待输入
STDIN
- perl:从步骤 3
dup2
发送STDOUT
到管道的写入端
- perl:
dup2
发送STDERR
到STDOUT
的目的地
- perl
exec
:perl
命令
- perl:写入输出并最终
exit
- sed:接收和编辑输入流
- sed:检测管道上的文件结尾
- sed: reap
perl
的退出状态waitpid
- 赛德:
exit
- shell:填充
$?
来自的返回值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
为我们做了很多工作,如perlfunc 文档open
中所述:
对于三个或更多参数,如果 MODE 为"|-"
,则文件名被解释为要通过管道输出的命令,如果 MODE 为"-|"
,则文件名被解释为通过管道输出给我们的命令。在双参数(和单参数)形式中,应将破折号 ( "-"
) 替换为命令。有关这方面的更多示例,请参阅在 perlipc 中使用open
IPC 。
它的输出是
$ ./简单管道
得到:再见
得到:你好
上面的代码硬编码了 的重复项STDOUT
,我们可以在下面看到。
$ ./simple-pipeline >/dev/null
Perl 反引号
要捕获另一个命令的输出,perl
请设置相同的机器,您可以在pp_backtick
(in pp_sys.c
) 中看到它,它调用Perl_my_popen
(in util.c
) 来创建子进程并设置管道 ( fork
, pipe
, dup2
)。孩子做了一些检查并调用Perl_do_exec3
(in doio.c
) 来启动我们想要输出的命令。我们注意到一个相关的评论:
/* handle the 2>&1 construct at the end */
该实现识别 sequence 2>&1
、 duplicatesSTDOUT
并从要传递给 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 '使用严格;使用警告;我的 $out=`DNE`; 打印 $out'
无法执行“DNE”:-e 第 1 行没有这样的文件或目录。
在 -e 第 1 行的打印中使用未初始化的值。
这里没有惊喜。
一个微妙的细节是很重要的理解。上面处理2>&1
内部的代码仅在要执行的命令的条件为真时运行:
if (*s != ' ' && !isALPHA(*s) &&
strchr("$&*(){}[]'\";\\|?<>~`\n",*s)) {
这是一个优化。如果反引号中的命令包含上述 shell 元字符,则perl
必须将其交给 shell。但是如果不存在 shell 元字符,perl
可以exec
直接使用命令——节省fork
shell 启动成本。
不存在的命令DNE
不包含 shell 元字符,perl
所有工作也是如此。由于命令失败并且您启用了warnings
编译指示,因此生成了 exec-category 警告。perlop 文档告诉我们,当命令失败时,反引号或在标量上下文中qx//
返回undef
,这就是为什么您会收到有关打印未定义值的警告的原因$out
。
缺少警告
$ perl -e '使用严格;使用警告;我的 $out=`DNE 2>&1`; 打印 $out'
在 -e 第 1 行的打印中使用未初始化的值。
失败的exec
警告去哪儿了?
记住创建运行另一个命令的子进程的基本步骤:
- 为子级创建一个管道以将其输出发送到父级。
- 调用
fork
创建一个几乎相同的子进程。
- 在孩子中,
dup2
连接STDOUT
到管道的写入端。
- 在子进程中,
exec
使新创建的子进程改为执行另一个程序。
- 在父级中,读取管道的内容。
要捕获另一个命令的输出,请执行perl
以下步骤。在准备尝试运行DNE 2>&1
时,perl
fork 一个子进程并在子进程中导致STDERR
复制STDOUT
,但还有另一个副作用。
if (!*t && (PerlLIO_dup2(1,2) != -1)) {
s[-2] = '\0';
break;
}
如果2>&1
是在命令的末尾并且dup2
成功,则perl
在重定向之前写入一个 NUL 字节。这具有将其从命令中删除的效果,例如,DNE 2>&1
变为DNE
! 现在,在命令中没有 shell 元字符的情况下,perl
子进程自己认为,“我们自己,我们可以exec
直接执行这个命令。”</p>
调用exec
失败,因为DNE
不存在。孩子仍然在 上发出失败的exec
警告STDERR
。它不会进入终端,因为dup2
它指向STDERR
与以下相同的位置STDOUT
:管道的写端返回父级。
父进程检测到子进程异常退出,并忽略管道的内容,因为命令执行失败的结果记录为undef
.
不同的警告
$ perl -e '使用严格;使用警告;我的 $out=`echo 123; DNE 2>&1`; 打印 $out'
123
sh:DNE:找不到命令
在这里,我们看到了DNE
不存在的不同诊断。遇到的第一个 shell 元字符是;
,因此perl
将未更改的命令交给 shell 执行。echo
正常完成,然后在shellDNE
中失败,并且 shell 的并返回父进程。从的角度来看,shell 执行得很好,所以没有什么需要警告的。STDOUT
STDERR
perl
相关说明
当您启用warnings
pragma(非常好的做法!)时,这将启用exec
警告类别。要查看这些警告的完整列表,请在perldiag文档中搜索字符串W exec
。
观察差异。
$ perl -Mstrict -Mwarnings -e '我的 $out=`DNE`; 打印 $out'
无法执行“DNE”:-e 第 1 行没有这样的文件或目录。
在 -e 第 1 行的 print 中使用未初始化的值 $out。
$ perl -Mstrict -Mwarnings -M-warnings=exec -e '我的 $out=`DNE`; 打印 $out'
在 -e 第 1 行的 print 中使用未初始化的值 $out。
后一个调用等价于
use strict;
use warnings;
no warnings 'exec';
my $out = `DNE`;
print defined($out) ? $out : "command failed\n";
exec
当 an 、 pipe等出现问题时,我喜欢格式化我自己的错误消息open
。这意味着我通常禁用 exec 警告,但这也意味着我必须格外小心地测试返回值。