如果反引号不能执行,STDERR重定向到STDOUT会丢失

时间:2013-02-07 16:00:42

标签: linux perl

如果命令无法执行,我发现在反引号调用中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。

3 个答案:

答案 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系统编程的细节。请继续阅读。

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及其派生词中,我们还可以将STDOUTSTDERR2>&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

顺序不同,但请记住STDERRSTDOUT是不同的流。另请注意,只有“hi”才能获得前缀。要获得这两行,它们都必须显示在STDOUT

$ perl -e 'print "hi\n"; warn "bye\n"' 2>&1 | sed -e 's/^/got: /'
got: bye
got: hi

要构造管道,shell会使用fork创建子进程,使用dup2执行重定向,并在适当的进程中调用exec来启动管道的每个阶段。对于上面的管道,该过程类似于

  • shell:fork运行sed
  • 的流程
  • shell:使用sed等待waitpid的退出状态
    • sed:创建pipe以向perl
    • 提供输入
    • sed:fork运行perl
    • 的流程
    • sed:dup2STDIN从管道的读取端读取
    • sed:exec sed命令
    • sed:等待STDIN的输入
      • perl:dup2STDOUT发送到第3步的管道写入端
      • perl:dup2STDERR发送到STDOUT的目的地
      • perl:exec perl命令
      • perl:写输出,最后是exit
    • sed:接收并编辑输入流
    • sed:检测管道上的文件结尾
    • sed:使用perl
    • 获取waitpid的退出状态
    • sed: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的首次调用为我们做了很多工作,如Using open for IPC in perlipc中所述:

  

对于三个或更多个参数,如果MODE为"|-",则文件名被解释为输出要通过管道输出的命令,如果MODE为"-|",则文件名被解释为命令管道输出给我们。在双参数(和单参数)形式中,应该用命令替换破折号("-")。有关此问题的更多示例,请参阅pp_backtick

它的输出是

$ ./simple-pipeline
got: bye
got: hi

上面的代码硬编码STDOUT的重复,我们可以在下面看到。

$ ./simple-pipeline >/dev/null

Perl反手

要捕获另一个命令的输出,perl设置相同的机制,您可以在Perl_my_popenpp_sys.c)中看到,该a relevant comment调用perldiag(在util.c)创建子流程并设置管道(forkpipedup2)。孩子做了一些管道并调用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警告在哪里?

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

  1. 为子项创建一个管道,以将其输出发送给父级。
  2. 致电fork以创建几乎相同的子流程。
  3. 在孩子中,dup2STDOUT连接到管道的写入端。
  4. 在孩子中exec使新创建的孩子改为执行另一个程序。
  5. 在父级中,读取管道的内容。
  6. 要捕获另一个命令的输出,perl会执行这些步骤。在准备尝试运行DNE 2>&1时,perl分叉一个孩子而在子进程中导致STDERRSTDOUT重复,但是还有另一个副作用。

    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警告。由于dup2STDERR指向与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的STDOUTSTDERR返回到父进程。从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::Open3IPC::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重定向到文件时,将打印输出。这很奇怪,但是很有效。