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

With perl open, how can I capture bash STDOUT & STDERR without printing to disk?

提问人:Jacob Wegelin 提问时间:3/16/2023 更新时间:5/5/2023 访问量:117

问:

下面的 perl 脚本使用 命令。findopen

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

如果用户在命令行上犯了错误,则将产生 STDERR。 但是,即使用户犯了错误,STDOUT 仍然可能包含有用的数据,就像我下面的示例一样。

因此,我们希望在 perl 脚本中有两个独立的变量,分别是 分别来自 unix 命令的 STDOUT 和 STDERR。

在当前脚本中,STDERR 只能通过首次打印获得 它复制到磁盘上的文件,然后读取该文件。

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

我知道可以在 perl 中使用而不是显式系统调用。但可以应用于其他 unix 命令,因此当前问题的答案可以应用于任意数量的 unix 命令。File::Findopen

> 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 系统 stderr 文件句柄

评论

0赞 Ted Lyngmo 3/16/2023
不相关,但不要在参数数组时.将数组提供给 。更安全!joinopenopen
0赞 simbabque 3/16/2023
你想要 Capture::Tiny。
0赞 Schwern 3/16/2023
您需要 IPC::Open3(Perl 附带)或 IPC::Run 或 Capture::Tiny。
0赞 Jacob Wegelin 3/16/2023
@TedLyngmo 如何更安全?当我将命令分解为数组时,它拒绝运行find: 2: unknown primary or operator $VAR1 = { 'command' => [ 'find', '-s', '.', '-maxdepth', '1', '2', '>', './z' ], <etc> }; $VAR1 = [];
2赞 Schwern 3/16/2023
@JacobWegelin 它更安全,因为当您传递单个字符串时,它会将其评估为 shell 命令,使其容易受到引用问题和 shell 漏洞的攻击。使用数组将参数直接传递给命令,而不进行 shell 解释。您的命令不起作用,因为您需要 shell 通过以下方式进行重定向2>

答:

3赞 ikegami 3/16/2023 #1

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. 设置第一个管道的 close-on-exec 标志。
    5. 打开。/dev/null
    6. 如果此操作失败,
      1. 将错误消息写入第一个管道。
      2. 叫。(退出代码无关紧要。_exit( 255 )
    7. 用于将手柄复制到 fd 0 上。dup2/dev/null
    8. 如果此操作失败,
      1. 将错误消息写入第一个管道。
      2. 叫。(退出代码无关紧要。_exit( 255 )
    9. 将手柄关闭到 。/dev/null
    10. 用于将第二个管道的写入端复制到 fd 1 上。dup2
    11. 如果此操作失败,
      1. 将错误消息写入第一个管道。
      2. 叫。(退出代码无关紧要。_exit( 255 )
    12. 关闭第二个管道的写入端。
    13. 用于将第二个管道的写入端复制到 fd 2 上。dup2
    14. 如果此操作失败,
      1. 将错误消息写入第一个管道。
      2. 叫。(退出代码无关紧要。_exit( 255 )
    15. 关闭第三个管道的写入端。
    16. exec程序。
    17. 如果这失败了(即如果我们到达这里),
      1. 将错误消息写入第一个管道。
      2. 叫。(退出代码无关紧要。_exit( 255 )
  6. 关闭第一个管道的写入端。
  7. 关闭第二个管道的写入端。
  8. 关闭第三个管道的写入端。
  9. 创建变量 。$error_msg
    1. 从第一个管道读取。
    2. 如果达到 EOF,
      1. 退出循环。
    3. 将已读的内容附加到 。$error_msg
  10. 如果定义,$error_msg
    1. 启动子项时出错。 包含原因。$error_msg
    2. 流产。
  11. 关闭第一个管道。
  12. 为第二个和第三个管道创建位域。select
  13. 创建两个初始化为空字符串的变量。每个管道都将存储从剩余的两个管道之一接收的数据。
  14. 当位域仍设置位时,
    1. 用于等待数据。select
    2. 对于每个现成的手柄,
      1. 用于从就绪手柄读取。sysread
      2. 如果此操作失败,(即如果返回 -1),sysread
        1. 这不应该发生。
        2. 清除位字段中的相应位。
      3. 如果 EOF(即如果返回零),sysread
        1. 清除位字段中的相应位。
        1. 将读取的数据追加到相应的变量中。
  15. 关闭第二根管道。
  16. 关闭第三根管道。
  17. 用于等待孩子退出。waitpid

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

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

1赞 zdim 3/16/2023 #2

还有简单的 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 代码,而不仅仅是通过 .因此,无需进行 shell 重定向(可以从命令中删除它)。system2> filename

这确实在内部使用文件,也可以显式管理(例如,这里详细讨论)。但我认为问题在于避免在代码中处理文件的需要。

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

0赞 Ted Lyngmo 3/16/2023 #3

我建议你像池上建议的那样使用。我只是想回答OP问的这部分问题:IPC::Run

那么我该如何自己重定向呢?

用于复制文件描述符。请注意,这仍然会写入单独的文件,并且仅回答重定向部分。open

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-sSTDERR./z

find: unknown predicate `-s'

评论

0赞 ikegami 3/16/2023
OP 询问如何避免使用临时文件,但您的解决方案使用临时文件...
0赞 Ted Lyngmo 3/16/2023
@ikegami 是的,它只回答了一个特定的部分。我更新了答案,使这一点更加清晰。