打开 perl,如何在不打印到磁盘的情况下捕获 bash STDOUT 和 STDERR?

问题描述 投票:0回答:3

下面的 perl 脚本调用 unix

find
使用
open
命令。

STDOUT 进入 CMD 文件句柄和 STDERR(如果有)进入磁盘上的临时文件。

STDERR 如果用户在命令行上犯了错误,将导致。 但即使用户犯了错误,STDOUT 仍然可能包含有用的数据,如下面的示例所示。

因此我们需要在 perl 脚本中有两个单独的变量,分别保存 分别来自 unix 命令的 STDOUT 和 STDERR。

当前脚本中,STDERR只能通过先打印获得 它到磁盘上的文件并随后读取文件。

有没有一种方法可以在单独的变量中捕获 STDOUT 和 STDERR,而无需将 STDERR 打印到磁盘上的物理文件的中间步骤?它能以某种方式直接打印到 perl 文件句柄或 perl 变量吗?

我知道可以在 perl 中使用

File::Find
而不是显式系统调用。但是
open
可以应用于其他 unix 命令,因此当前问题的答案可以应用于任意数量的 unix 命令。

> cat z.pl
#!/usr/bin/env perl
use strict; use warnings;
use Data::Dumper qw(Dumper); $Data::Dumper::Sortkeys = 1;

my$work={};
$work->{tmpfile}='./z';
$work->{command}=join(' ',
    'find -s ',
    @ARGV,
    '2>',
    $work->{tmpfile},
);

open CMD, '-|', $work->{command} or die $work->{command},' cannot open';
$work->{return}{open}=$?;
my@result;
while(<CMD>)
{
    chomp;
    push@result,$_;
}
close CMD;
$work->{return}{close}=$?;
if(-s $work->{tmpfile})
{
    open IN,'<',$work->{tmpfile} or die 'cannot read ',$work->{tmpfile},;
    local$/=undef;
    $work->{stderr}=<IN>;
    close IN;
}
print Dumper $work;
print Dumper \@result;
> ./z.pl . piggy
$VAR1 = {
          'command' => 'find -s  . piggy 2> ./z',
          'return' => {
                        'close' => 256,
                        'open' => 0
                      },
          'stderr' => 'find: piggy: No such file or directory
',
          'tmpfile' => './z'
        };
$VAR1 = [
          '.',
          './.z.pl.swp',
          './body',
          './body~',
          './snap',
          './title',
          './z',
          './z.pl',
          './z.pl~'
        ];
>
bash perl system stderr filehandle
3个回答
2
投票

IPC::Run 让这一切变得简单。

use IPC::Run qw( run );

my @cmd = ( "find", "-s", ".", "piggy" );

run \@cmd, ">", \my $stdout, "2>", \my $stderr;

my $status = $?;

孩子继承了上面程序中的STDIN。如果不需要,您可以使用以下内容:

run \@cmd, "/dev/null", \my $stdout, \my $stderr;

但是如果您有兴趣了解所涉及的系统调用,请继续阅读。

  1. 创建三个管道。
  2. 如果失败,
    1. 中止。
  3. 叉子。
  4. 如果失败,
    1. 中止。
  5. 在孩子身上,
    1. 关闭第一个管道的读取端。
    2. 关闭第二个管道的读取端。
    3. 关闭第三个管道的读取端。
    4. 设置第一个管道的关闭执行标志。
    5. 打开
      /dev/null
      .
    6. 如果失败,
      1. 向第一个管道写入错误消息。
      2. 呼叫
        _exit( 255 )
        。 (退出代码无关紧要。)
    7. 使用
      dup2
      将句柄复制到
      /dev/null
      到fd 0.
    8. 如果失败,
      1. 向第一个管道写入错误消息。
      2. 呼叫
        _exit( 255 )
        。 (退出代码无关紧要。)
    9. 将手柄关闭到
      /dev/null
      .
    10. 使用
      dup2
      将第二个管道的写入端复制到fd 1.
    11. 如果失败,
      1. 向第一个管道写入错误消息。
      2. 呼叫
        _exit( 255 )
        。 (退出代码无关紧要。)
    12. 关闭第二个管道的写端。
    13. 使用
      dup2
      将第二个管道的写入端复制到fd 2.
    14. 如果失败,
      1. 向第一个管道写入错误消息。
      2. 呼叫
        _exit( 255 )
        。 (退出代码无关紧要。)
    15. 关闭第三个管道的写端。
    16. exec
      程序。
    17. 如果失败(即如果我们到达这里),
      1. 向第一个管道写入错误消息。
      2. 呼叫
        _exit( 255 )
        。 (退出代码无关紧要。)
  6. 关闭第一个管道的写端。
  7. 关闭第二个管道的写端。
  8. 关闭第三个管道的写端。
  9. 创建变量
    $error_msg
    .
  10. 循环,
    1. 从第一个管道读取。
    2. 如果达到EOF,
      1. 退出循环。
    3. 将阅读的内容附加到
      $error_msg
      .
  11. 如果定义了
    $error_msg
    1. 启动孩子时出错。
      $error_msg
      包含原因。
    2. 中止。
  12. 关闭第一个管道。
  13. 为第二个和第三个管道创建一个
    select
    位域。
  14. 创建两个初始化为空字符串的变量。每个都将存储从剩余的两个管道之一接收到的数据。
  15. 虽然位域仍然有位设置,
    1. 使用
      select
      等待数据。
    2. 对于每个准备好的手柄,
      1. 使用
        sysread
        从准备好的手柄读取。
      2. 如果失败,(即如果
        sysread
        返回 -1),
        1. 这不应该发生。
        2. 清除位域中的适当位。
      3. 如果 EOF(即如果
        sysread
        返回零),
        1. 清除位域中的适当位。
      4. 否则,
        1. 将读取的数据附加到适当的变量。
  16. 关闭第二个管道。
  17. 关闭第三个管道。
  18. 使用
    waitpid
    等待孩子退出

IPC::Open3 的

open3
可以为您处理步骤 1 到 12。

IO::Select 可以为您简化步骤 13 到 15。


0
投票

我建议你像

ikegami建议的那样使用
IPC::Run。我只想回答这个:

那我怎么自己重定向它

sub run {
    my $output = shift; # a file to redirect STDERR to

    print "Running: ('" . join("', '", @_) . "') 2> $output\n";

    # dup STDERR so that we can restore it later
    open(my $err, '>&', \*STDERR) or die "Can't dup stderr: $!";

    # reopen STDERR to output to the supplied file
    open(STDERR, '>', $output) or die "Can't redirect stderr: $!";

    # "open" your command
    open(my $cmd, '-|', @_) or die "Can't exec $_[0]: $!";
    while(<$cmd>) {
        print $_;
    }
    close($cmd);

    # restore STDERR
    open(STDERR, '>&', $err) or die "Can't restore stderr: $!";
    close($err);
}

my @cmd = ('find', '-s', '.', '-maxdepth', '1', '2');

run './z', @cmd;

STDOUT
上的输出:

Running: ('find', '-s', '.', '-maxdepth', '1', '2') 2> ./z

我的

find
不知道
-s
所以在
STDERR
上的输出(因此
./z
的内容)变成了:

find: unknown predicate `-s'

0
投票

还有简单的Capture::Tiny

use warnings;
use strict;
use feature 'say';

use Capture::Tiny qw(capture);

#my @cmd = ('find', '-s', @ARGV);
my @cmd = ( 'ls', '-l', (@ARGV ? shift : '.') );    

my ($stdout, $stderr, $exit) = capture {
  system( @cmd );
};

say $stdout          if $stdout;
say "Error: $stderr" if $stderr;
say "exit: $exit"    if $exit;

一个人可以在块内运行任何 Perl 代码,而不仅仅是通过

system
.

的外部命令

请小心将用户输入作为参数传递给命令。

© www.soinside.com 2019 - 2024. All rights reserved.