How do I use Perl IPC::Run3 to read stdout and stderr from a subprocess?

Viewed 277

I want to run a make command from a Perl script so I can capture its stdout and stderr streams. I know I could use open(MAKE, "make 2>&1 |") but this gives problems building the right shell command to pass arguments to make, and using open(MAKE, "-|", @makecmd, "2>&1") doesn't work because passing the command as an array doesn't spawn a subshell to do the redirection.

I came across IPC::Run3 and I've got it working, but my use of filehandles is ugly - basically I've had to spawn a cat subprocess to get a handle that I can tell IPC::Run3 to write to so my script can read from it, and my attempts at passing STDIN for this purpose have failed. What am I doing wrong?

#!/usr/bin/perl

use strict;
use warnings;
use IPC::Run3;

#my $pipe = \*STDIN;           #-- this produces no output and hangs
#open(my $pipe, "<&STDIN");    #-- this outputs "foo bar" and hangs
open(my $pipe, "|cat");        #-- this works, but extra process is ugly

run3 "echo foo; echo bar >&2", \undef, $pipe, $pipe;

while (<$pipe>) {
    print ">>> $_";
}
3 Answers

You don't. You use IPC::Run instead. Naïve use of IPC::Open3 leaves you open to deadlocks. Avoiding this involves using IO::Select or some other mechanism. The work involved is extensive. IPC::Open3 is just too low-level for practical use.


That said, you're only dealing with one file handle. That can be done relatively simply using open3.

use IPC::Open3 qw( open3 );

open(local *CHILD_STDIN, '<', '/dev/null') or die $!;
*CHILD_STDIN if 0;
pipe(local (*READER, *WRITER)) or die $!;
my $pid = open3('<&CHILD_STDIN', '>&WRITER', '>&WRITER', @cmd);

close(WRITER);

while (<READER>) {
   ...
}

waitpid($pid);

Yuck! It's much cleaner using IPC::Run.

use IPC::Run qw( run );

run \@cmd, \undef, '>pipe', \my $pipe, '2>&1';

while (<$pipe>) {
   ...
}

close($pipe);

Well, that's what the documentation says, but it doesn't work. You actually need

use IPC::Run qw( run );
use Symbol   qw( gensym );

run \@cmd, \undef, '>pipe', (my $pipe = gensym), '2>&1';

while (<$pipe>) {
   ...
}

close($pipe);

And if you want all the output, you could simply use

use IPC::Run qw( run );

run \@cmd, \undef, \my $output;

Finally, you mention problems building shell commands.

What you were looking for is

use String::ShellQuote qw( shell_quote );

my $cmd = shell_quote(@cmd) . ' 2>&1';
open(my $pipe, '-|', $cmd);

while (<$pipe>) {
   ...
}

close($pipe);

Thanks to @ikegami I settled on this...

#!/usr/bin/perl

use strict;
use warnings;
use IPC::Run qw(run);       # CPAN or yum install perl-IPC-Run
use Symbol qw(gensym);

run ["sh", "-c", "echo foo; echo bar >&2"], \undef, '>pipe', (my $pipe = gensym), '2>&1';

while (<$pipe>) {
    print ">>> $_";
}

Or, if gensym is a bit under-the-hood for you...

#!/usr/bin/perl

use strict;
use warnings;
use IPC::Run qw(run);       # CPAN or yum install perl-IPC-Run
use IO::Handle;

run ["sh", "-c", "echo foo; echo bar >&2"], \undef, '>pipe', (my $pipe = new IO::Handle), '2>&1';

while (<$pipe>) {
    print ">>> $_";
}

IPC::Run3 can be used if you create an IO::Pipe and pass the "writer" filehandle to it, but that involves forking your main process so I wouldn't recommend it, though it does answer my original question. @ikegami's solution with IPC::Run is much more elegant.

#!/usr/bin/perl

use strict;
use warnings;
use IPC::Run3;              # CPAN or yum install perl-IPC-Run3
use IO::Pipe;

my $pipe = new IO::Pipe;

if(my $pid = fork()) {
    # Parent
    $pipe->reader();

    while (<$pipe>) {
        print ">>> $_";
    }
}
elsif (defined $pid) {
    # Child ($pid = 0)
    $pipe->writer();
    run3 "echo foo; echo bar >&2", \undef, $pipe, $pipe;
}
else {
    # fork() failed
    die "failed to fork - $!";
}
Related