代码之家  ›  专栏  ›  技术社区  ›  Jonas Wagner

Perl:避免贪婪地读取stdin?

  •  4
  • Jonas Wagner  · 技术社区  · 15 年前

    read.pl ):

    my $line = <STDIN>;
    print "Perl read: $line";
    print "And here's what cat gets: ", `cat -`;
    

    cat 在输入结束前获取其他所有内容( ^D 按下)。

    $ echo "foo\nbar" | ./read.pl
    Perl read: foo
    And here's what cat gets:
    

    Perl似乎在某个地方对整个输入进行了极大的缓冲,而使用backticks或system调用的进程看不到任何输入。

    <STDIN>

    5 回复  |  直到 15 年前
        1
  •  2
  •   Chas. Owens    15 年前

    这不是Perl问题。这是一个UNIX/shell问题。当您在没有管道的情况下运行命令时,您处于行缓冲模式,但是当您使用管道重定向时,您处于块缓冲模式。你可以这样说:

    cat /usr/share/dict/words | ./read.pl | head
    

    这个C程序有同样的问题:

    #include <stdio.h>
    
    int main(int argc, char** argv) {
        char line[4096];
        FILE* cat;
        fgets(line, 4096, stdin);
        printf("C got: %s\ncat got:\n", line);
        cat = popen("cat", "r");
        while (fgets(line, 4096, cat)) {
            printf("%s", line);
        }
        pclose(cat);
        return 0;
    }
    
        2
  •  2
  •   Greg Bacon    15 年前

    我有好消息和坏消息。

    好消息是对 read.pl 允许您提供假输入:

    #! /usr/bin/perl
    
    use warnings;
    use strict;
    
    binmode STDIN, "unix" or die "$0: binmode: $!";
    
    my $line = <STDIN>;
    print "Perl read: $line";
    print "And here's what cat gets: ", `cat -`;
    

    运行示例:

    $ printf "A\nB\nC\nD\n" | ./read.pl 
    Perl read: A
    And here's what cat gets: B
    C
    D

    坏消息是你得到了一个单一的转换:如果你试图重复读然后猫,第一 cat

    #! /usr/bin/perl
    
    use warnings;
    use strict;
    
    binmode STDIN, "unix" or die "$0: binmode: $!";
    
    my $line = <STDIN>;
    print "1: Perl read: $line";
    print "1: And here's what cat gets: ", `cat -`;
    $line = <STDIN>;
    $line = "<undefined>\n" unless defined $line;
    print "2: Perl read: $line";
    print "2: And here's what cat gets: ", `cat -`;
    

    然后是产生

    $ printf "A\nB\nC\nD\n" | ./read.pl 
    1: Perl read: A
    1: And here's what cat gets: B
    C
    D
    2: Perl read: <undefined>
    2: And here's what cat gets: 
        3
  •  2
  •   Jonas Wagner    15 年前

    Expect 这是完美的情况下:

    #!/usr/bin/perl
    
    use strict;
    use warnings;
    
    use Expect;
    
    my $exp = Expect->spawn('./read.pl');
    $exp->send("First Line\n");
    $exp->send("Second Line\n");
    $exp->send("Third Line\n");
    $exp->soft_close();
    

    像个魔咒一样工作;)

        4
  •  0
  •   Jonas Wagner    15 年前

    use IPC::Run;
    
    my $input = "First Line\n";
    my $output;
    my $process = IPC::Run::start(['./read.pl'], \$input, \$output);
    $process->pump() until $output =~ /Perl read:/;
    $input .= "Second Line\n";
    $process->finish();
    print $output;
    

    在等待更多输入之前,需要知道程序将发出的“提示”,这是次优的。

    另一个次优解决方案如下:

    use IPC::Run;
    
    my $input = "First Line\n";
    my $output;
    my $process = IPC::Run::start(['./read.pl'], \$input, my $timer = IPC::Run::timer(1));
    $process->pump() until $timer->is_expired();
    $timer->start(1);
    $input .= "Second Line\n";
    $process->finish();
    

    它不需要任何提示信息,但是速度很慢,因为它至少要等待两秒钟。另外,我不明白为什么需要第二个计时器(否则finish不会返回)。

    有人知道更好的解决办法吗?

        5
  •  0
  •   Community Mohan Dere    9 年前

    最后我得到了以下解决方案。虽然还远未达到最佳状态,但确实有效。即使在这样的情况下 the one described by gbacon .

    use Carp qw( confess );
    use IPC::Run;
    use Scalar::Util;
    use Time::HiRes;
    
    # Invokes the given program with the given input and argv, and returns stdout/stderr.
    #
    # The first argument provided is the input for the program. It is an arrayref
    # containing one or more of the following:
    # 
    # * A scalar is simply passed to the program as stdin
    #
    # * An arrayref in the form [ "prompt", "input" ] causes the function to wait
    #   until the program prints "prompt", then spools "input" to its stdin
    #
    # * An arrayref in the form [ 0.3, "input" ] waits 0.3 seconds, then spools
    #   "input" to the program's stdin
    sub capture_with_input {
        my ($program, $inputs, @argv) = @_;
        my ($stdout, $stderr);
        my $stdin = '';
    
        my $process = IPC::Run::start( [$program, @argv], \$stdin, \$stdout, \$stderr );
        foreach my $input (@$inputs) {
            if (ref($input) eq '') {
                $stdin .= $input;
            }
            elsif (ref($input) eq 'ARRAY') {
                (scalar @$input == 2) or
                    confess "Input to capture_with_input must be of the form ['prompt', 'input'] or [timeout, 'input']!";
    
                my ($prompt_or_timeout, $text) = @$input;
                if (Scalar::Util::looks_like_number($prompt_or_timeout)) {
                    my $start_time = [ Time::HiRes::gettimeofday ];
                    $process->pump_nb() while (Time::HiRes::tv_interval($start_time) < $prompt_or_timeout);
                }
                else {
                    $prompt_or_timeout = quotemeta $prompt_or_timeout;
                    $process->pump until $stdout =~ m/$prompt_or_timeout/gc;
                }
    
                $stdin .= $text;
            }
            else {
                confess "Unknown input type passed to capture_with_input!";
            }
        }
        $process->finish();
    
        return ($stdout, $stderr);
    }
    
    my $input = [
        "First Line\n",
        ["Perl read:", "Second Line\n"],
        [0.5, "Third Line\n"],
    ];
    print "Executing process...\n";
    my ($stdout, $stderr) = capture_with_input('./read.pl', $input);
    print "done.\n";
    print "STDOUT:\n", $stdout;
    print "STDERR:\n", $stderr;
    

    用法示例(略加修改阅读.pl为了测试巴肯的情况):

    $ time ./spool_read4.pl
    Executing process...
    done.
    STDOUT:
    Perl read: First Line
    And here's what head -n1 gets: Second Line
    Perl read again: Third Line
    
    STDERR:
    ./spool_read4.pl  0.54s user 0.02s system 102% cpu 0.547 total
    

    不过,我对更好的解决方案持开放态度。。。