您的语法是正确的,但在一种情况下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. (为完整起见,文件描述符为STDIN0。)
在 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 中使用openIPC 。
它的输出是
$ ./简单管道
得到:再见
得到:你好
上面的代码硬编码了 的重复项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直接使用命令——节省forkshell 启动成本。
不存在的命令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时,perlfork 一个子进程并在子进程中导致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 执行得很好,所以没有什么需要警告的。STDOUTSTDERRperl
相关说明
当您启用warningspragma(非常好的做法!)时,这将启用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 警告,但这也意味着我必须格外小心地测试返回值。