【问题标题】:Perl: How to pass IPC::Open3 redirected STDOUT/STDERR fhsPerl:如何通过 IPC::Open3 重定向 STDOUT/STDERR fhs
【发布时间】:2019-03-25 12:50:05
【问题描述】:

我正在尝试捕获我的 perl 代码从 print 和类似语句以及外部命令生成的输出。

由于设计限制,我无法使用 Capture::Tiny 之类的解决方案。我需要在生成后立即将输出转发到缓冲区变量,并且我需要能够区分 STDOUTSTDERR。理想情况下,外部命令的解决方案基本上可以像系统一样工作,除了能够捕获 STDOUTSTDERR 而不是打印它们。

我的代码应该是:

  1. 保存旧的 STDOUT/STDERR 文件句柄。
  2. STDERRSTDOUT 创建一个新的。
  3. 将所有输出重定向到此位置。
  4. 打印几样东西。
  5. 恢复旧的文件句柄。
  6. 对捕获的输出做一些事情,例如打印出来。

但是,我无法捕获从外部命令生成的输出。我不能用IPC::Run3IPC::Open3 来做。

#!/usr/bin/perl -CSDAL
use warnings;
use strict;
use IPC::Open3;
#use IPC::Run3;

# Save old filehandles
open(my $oldout, ">&STDOUT") or die "Can't dup STDOUT: $!";
open(my $olderr, ">&STDERR") or die "Can't dup STDERR: $!";

my $buffer = "";

close(STDOUT);
close(STDERR);

open(STDOUT, '>', \$buffer) or die "Can't redirect STDOUT: $!";
*STDERR = *STDOUT; # In this example STDOUT and STDERR are printed to the same buffer.

print "1: Test\n";
#run3 ["date"], undef, \*STDOUT, \*STDERR; # This doesn't work as expected
my $pid = open3("<&STDIN", ">&STDOUT", ">&STDERR", "date");
waitpid($pid,0); # Nor does this.

print STDERR "2: Test\n";

open(STDOUT, ">&", $oldout) or die "Can't dup \$oldout: $!";
open(STDERR, ">&", $olderr) or die "Can't dup \$olderr: $!";

print "Restored!\n";
print $buffer;

预期结果:

Restored!
1: Test
Mo 25. Mär 13:44:53 CET 2019
2: Test

实际结果:

Restored!
1: Test
2: Test

【问题讨论】:

  • 也许open3 无法处理字符串缓冲区句柄?为什么不对open3 调用使用常规文件句柄?
  • 使用 eval 我可以得到 run3 语句的错误消息:Error: run3(): Invalid argument redirecting STDOUT at /root/test_io.pl line 25. 这对我没有帮助。如果不重新分配 STDOUTSTDERR,代码将按预期工作。我在这里有点茫然。
  • @HåkonHægland 我尽量避免这种情况,因为我不想要数百个临时文件。
  • 您能否为我澄清/确认——您想简单地说,重定向标准输出和标准错误(分别),用于来自程序和来自外部命令的打印吗?就是这样,重定向流?这是相当简单的,在几个方面。 (必须是缓冲区(变量)而不是文件吗?)

标签: linux perl


【解决方案1】:

我无法为您提供解决方案,但我可以就您所看到的行为提供一些解释。

首先,当您的文件句柄是变量时,IPC::Open3 不应该工作;更多解释见this question

现在,为什么IPC::Run3 不起作用?首先,请注意如果不重定向STDERR 并运行

run3 ["date"], undef, \$buffer, { append_stdout => 1 };

而不是

run3 ["date"], undef, \*STDOUT;

然后它按预期工作。 (您需要添加{ append_stdout =&gt; 1 } 否则您之前的输出到$buffer 将被覆盖)

要了解在你的程序中发生了什么,

open(STDOUT, '>', \$buffer) or die "Can't redirect STDOUT: $!";

添加

print STDERR ref(\$buffer), "\n"
print STDERR ref(\*STDOUT), "\n"

哪个会打印

SCALAR
GLOB

这正是IPC::Run3::run3 要知道如何处理你给它的“stdout”所做的事情(见源代码:_fh_for_child_output,由run3 调用):

这就解释了为什么当你用\$buffer 调用run3 时它可以工作,但不能用\*STDOUT


同时重定向STDERR,并调用

run3 ["date"], undef, \$buffer, \$buffer, { append_stdout => 1, append_stderr => 1 };

,事情开始变得很奇怪。我不明白发生了什么,但我会在这里分享我的发现,希望有人能理解它。

我修改了IPC::Run3的来源并添加了

open my $FP, '>', 'logs.txt' or die "Can't open: $!";

在子 run3 的开头。运行时,我只看到

Restored!
1: Test

在 STDOUT(我的终端)上,但 logs.txt 包含日期(Mon Mar 25 17:49:44 CET 2019 行中的内容)。

投入一点发现fileno $FP 返回1(除非我弄错了,通常是STDOUT(但你关闭了它,所以我对它的描述符可以重复使用并不感到惊讶)),并且fileno STDOUT 返回 2(这可能取决于您的 Perl 版本和其他打开的文件句柄)。 似乎正在发生的是system 假定STDOUT 是文件描述符1,因此打印到$FP 而不是STDOUT(我只是在猜测)。

如果您了解正在发生的事情,请随时发表评论/编辑。

【讨论】:

  • 致比我更了解的人:IPC::Run3 的这种行为是否应该被视为错误?
  • 遗憾的是,这个解决方案似乎没有达到预期的效果。只要我实际上不重定向 STDOUT 和 STDERR 并且只重定向 run3 的输出,就可以正常工作。但是,一旦我这样做,调用就会失败:run3():恢复STDOUT的错误文件描述符,run3():在./testio2.pl第22行恢复STDERR的错误文件描述符
  • @K.A.B.确实,我的错,我很清楚为什么会发生这种情况;我正在尝试修复它。
  • 这不是错误,您必须将文件句柄传递给 IPC::Open3。
  • 您链接到的问题的页面还显示了一种工作方式(在另一个答案中),使用IPC::Run3(此处显然可以接受)。跨度>
【解决方案2】:

我最终得到了以下代码:

#!/usr/bin/perl -CSDAL
use warnings;
use strict;
use IPC::Run3;
use IO::Scalar;
use Encode;
use utf8;

# Save old filehandles
open(my $oldout, ">&STDOUT") or die "Can't dup STDOUT: $!";
open(my $olderr, ">&STDERR") or die "Can't dup STDERR: $!";

open(my $FH, "+>>:utf8", undef) or die $!;
$FH->autoflush;

close(STDOUT);
close(STDERR);

open(STDOUT, '>&', $FH) or die "Can't redirect STDOUT: $!";
open(STDERR, '>&', $FH) or die "Can't redirect STDOUT: $!";

print "1: Test\n";

run3 ["/bin/date"], undef, $FH, $FH, { append_stdout => 1, append_stderr => 1 };

print STDERR "2: Test\n";

open(STDOUT, ">&", $oldout) or die "Can't dup \$oldout: $!";
open(STDERR, ">&", $olderr) or die "Can't dup \$olderr: $!";

print "Restored!\n";
seek($FH, 0, 0);
while(<$FH>)
{
  # No idea why this is even required
  print Encode::decode_utf8($_);
}
close($FH);

这与我最初想要的相差甚远,但似乎至少可以工作。

我遇到的问题是:

  1. 我需要一个匿名文件句柄,这会在硬盘上造成混乱。
  2. 出于某种原因,我需要手动修复编码。

非常感谢在这里投入时间帮助我的人们。

【讨论】:

  • binmode_stdin =&gt; ":utf8", binmode_stdout =&gt; ":utf8", binmode_stderr =&gt; ":utf8" 添加到 run3 选项,谢天谢地解决了我的问题。
【解决方案3】:

您是否有理由需要使用父级的 STDOUT 和 STDERR? IPC::Open3 很容易将子级的 STDOUT 和 STDERR 重定向到父级中不相关的句柄,您可以从中读取。

use strict;
use warnings;
use IPC::Open3;

my $pid = open3 undef, my $outerr, undef, 'date';
my $output = do { local $/; readline $outerr };
waitpid $pid, 0;
my $exit = $? >> 8;

这将一起读取 STDOUT 和 STDERR,如果您想单独读取它们,您需要将 my $stderr = Symbol::gensym 作为第三个参数传递(如 IPC::Open3 文档中所示),并使用非阻塞循环来避免读取两个句柄时出现死锁。 IO::Async::Process 或类似的东西可以为您完全自动化,但如果您只需要将输出存储在标量变量中,IPC::Run3 提供了一个更简单的解决方案。 IPC::Run3 和 Capture::Tiny 也可以很容易地成为 fatpacked 在脚本中部署。

【讨论】:

  • 请注意,在 5.14 perl -MIPC::Open3 -we'open(my $fh, "&lt;", "/dev/null") or die $!; open3("&lt;&amp;".fileno($fh), "&gt;&amp;STDOUT", "&gt;&amp;STDERR", "cat")' throw open3: close(3) failed: Bad file descriptor at -e line 1 之前使用 STDIN 的词法句柄有问题。
  • @ikegami 该语法是复制句柄,它也不适用于在内存中打开的句柄,因为它们没有 fd - 我的示例不是指那个,而是传递词法句柄以直接使用.
  • 您的帖子表明,在处理 IPC::Open2/3 时,词法与 glob 一样好,但在 5.14 之前情况并非如此。而已。只是一个评论,以防它对某人很重要。我不是说你的帖子应该被编辑。
【解决方案4】:

这还不是答案,但似乎open3 要求STDOUT 在您调用open3 时成为常规的tty 文件句柄,例如:

use feature qw(say);
use strict;
use warnings;

use IPC::Open3;
use Symbol 'gensym';
{
    local *STDOUT;  # <-- if you comment out this line open3 works as expected
    my ($chld_in, $chld_out);
    my $chld_err = gensym;
    my $pid;
    eval {
        $pid = open3($chld_in, $chld_out, $chld_err, "date");
    };
    if ( $@ ) {
        say "IPC::Open::open3 failed: '$@'";
    }
    print "-> $_" for <$chld_out>;
    waitpid $pid, 0;
   # say "Cannot print to invalid handle..";
}
say "ok";

输出

ma. 25. mars 16:00:01 +0100 2019
ok

请注意,缺少行首的箭头-&gt;, 所以在这种情况下,无法从$chld_out 读取任何内容。但是,如果我注释掉该行:

local *STDOUT;

输出是:

-> ma. 25. mars 16:01:10 +0100 2019
ok

【讨论】:

  • 此代码存在竞争条件。如果孩子向其 STDERR 发送了足够的金额,则孩子和父母都将永远阻塞。
  • @ikegami 不错。我猜你的意思是应该实现一个 select 循环来检查 STDOUT or STDERR 是否准备好阅读?然后从准备好的句柄中读取
  • 使用IO::Async::Process 之类的东西走这条路会更好,但如果 IPC::Run3 或 IPC::Run 能满足您的需要,它们会简单得多。
猜你喜欢
  • 2017-07-14
  • 2013-07-16
  • 2014-07-09
  • 1970-01-01
  • 1970-01-01
  • 2012-08-17
  • 2014-07-22
  • 1970-01-01
  • 2022-01-18
相关资源
最近更新 更多