繁体   English   中英

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

[英]STDERR redirection to STDOUT lost if backticks can't exec

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

您的语法是正确的,但在一种情况下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文件描述符。 (为完整STDINSTDIN文件描述符为0.)

在Bourne shell及其衍生产品中,我们还可以将STDOUTSTDERR合并为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的包装器。

重定向和管道

现在说我们要为命令输出的每一行添加一个前缀。

$ 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:使用waitpid等待sed退出状态
    • sed:创建一个pipe以将输入提供给perl
    • sed: fork一个进程运行perl
    • sed: dup2使STDIN从管道的读取端读取
    • sed: exec sed命令
    • sed:等待STDIN输入
      • perl: dup2STDOUT从步骤3发送到管道的写端
      • perl: dup2STDERR发送到STDOUT的目的地
      • perl: exec perl命令
      • perl:写输出并最终exit
    • sed:接收和编辑输入流
    • sed:检测管道上的文件结束
    • sed:用waitpid收获perl的退出状态
    • 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对我们来说做了很多工作,如openperlfunc文档中所述:

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

它的输出是

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

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

$ ./simple-pipeline >/dev/null

Perl反引号

为了捕获另一个命令的输出, perl设置了相同的机制,你可以在pp_backtick (在pp_sys.c )看到它,它调用Perl_my_popen (在util.c )来创建子进程并设置管道( forkpipedup2 )。 孩子做了一些管道并调用Perl_do_exec3 (在doio.c )来启动我们想要的输出命令。 我们注意到相关评论

/* 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编译指示。 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中, exec使新创建的子进程执行另一个程序。
  5. 在父级中,读取管道的内容。

为了捕获另一个命令的输出, perl完成了这些步骤。 在准备尝试运行DNE 2>&1perl分叉一个孩子并且在子进程中导致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不存在。 孩子仍然在STDERR上发出失败的exec警告。 由于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 Very Good Practice! - 这将启用exec警告类别。 要查看这些警告的完整列表,请在perldiag文档中搜索字符串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警告,但这也意味着我必须非常小心地测试返回值。

这里的问题是重定向是由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
  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重定向到文件时,将打印输出。 这很奇怪,但是很有效。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM