Perl: избежать жадного чтения со стандартного ввода? - PullRequest
4 голосов
/ 13 сентября 2010

Рассмотрим следующий скрипт на Perl (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 кажется ужасногде-то буферизует весь ввод данных, а процессы, вызываемые с помощью обратных кавычек или системы, не видят ничего из ввода.

Проблема в том, что я хотел бы выполнить модульное тестирование сценария, который смешивает <STDIN> и вызывает другие процессы,Каков был бы лучший способ сделать это?Можно ли отключить входную буферизацию в perl?Или я могу буферизовать данные так, чтобы они «имитировали» терминал?

Ответы [ 5 ]

2 голосов
/ 15 сентября 2010

Сегодня я думаю, что нашел то, что мне нужно: в Perl есть модуль под названием 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();

Работает как шарм;)

2 голосов
/ 13 сентября 2010

Это не проблема Perl.Это проблема UNIX / оболочки.Когда вы запускаете команду без каналов, вы находитесь в режиме буферизации строк, но когда вы перенаправляете с каналами, вы находитесь в режиме буферизации блоков.Вы можете увидеть это, сказав:

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 голосов
/ 13 сентября 2010

У меня есть хорошие новости и плохие новости.

Хорошие новости - это простая модификация 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: 
0 голосов
/ 14 сентября 2010

Наконец я остановился на следующем решении.Все еще далеко от оптимального, но это работает.Даже в таких ситуациях, как описанный 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;

Пример использования (со слегка измененным read.pl для проверки случая gbacon):

$ 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

Тем не менее, я открыт для лучших решений ...

0 голосов
/ 13 сентября 2010

Вот неоптимальный способ, который я нашел:

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();

Это не требует знания какого-либо приглашения, но медленное, поскольку оно ожидает не менее двух секунд.Кроме того, я не понимаю, зачем нужен второй таймер (иначе обработчик не вернется).

Кто-нибудь знает лучшие решения?

...