5

我发现STDERR如果命令执行失败,反引号调用中的重定向可能会丢失。我被我看到的行为难住了。

$ perl -e '使用严格;使用警告;我的 $out=`DNE`; 打印 $out'  
无法执行“DNE”:-e 第 1 行没有这样的文件或目录。
在 -e 第 1 行的打印中使用未初始化的值。

$ perl -e '使用严格;使用警告;我的 $out=`DNE 2>&1`; 打印 $out'
在 -e 第 1 行的打印中使用未初始化的值。

$ perl -e '使用严格;使用警告;我的 $out=`echo 123; DNE 2>&1`; 打印 $out'
123
sh:DNE:找不到命令

我的语法不正确吗?

我在 Linux 上使用 Perl 5.8.5。

4

3 回答 3

16

您的语法是正确的,但在一种情况下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”'

它的输出是

你好
再见

请注意,输出printSTDOUT标准输出,并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 及其派生类中,我们还可以将STDOUTSTDERR与合并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:dup2STDIN管道的读取端读取
    • sed execsed命令
    • sed:等待输入STDIN
      • perl:从步骤 3dup2发送STDOUT到管道的写入端
      • perl:dup2发送STDERRSTDOUT的目的地
      • perl execperl命令
      • perl:写入输出并最终exit
    • sed:接收和编辑输入流
    • sed:检测管道上的文件结尾
    • sed: reapperl的退出状态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警告去哪儿了?

记住创建运行另一个命令的子进程的基本步骤:

  1. 为子级创建一个管道以将其输出发送到父级。
  2. 调用fork创建一个几乎相同的子进程。
  3. 在孩子中,dup2连接STDOUT到管道的写入端。
  4. 在子进程中,exec使新创建的子进程改为执行另一个程序。
  5. 在父级中,读取管道的内容。

要捕获另一个命令的输出,请执行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 警告,但这也意味着我必须格外小心地测试返回值。

于 2013-02-07T16:33:45.373 回答
8

这里的问题是重定向是由 shell 完成的。而且你的`...` 命令不是通过shell 运行的——Perl 尝试使用$PATH 查找DNE 程序,但它失败了。

如果您需要同时捕获 stdout 和 stderr,您可以采用多种方式,但我认为最安全的是使用IPC::Open3IPC::Run

如果你觉得冒险,你可以尝试做这样的事情,但请记住这是个坏主意:

$ perl -e 'use strict; use warnings; my $o=`sh -c "DNE 2>&1"`; print $o' 
sh: 1: DNE: not found
于 2013-02-07T16:13:01.747 回答
2
  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 重定向到文件时,会打印输出。这很奇怪,但有效。

于 2013-02-08T10:56:29.417 回答