#################################################################
#
# IPC::Open3::Simple - A simple alternative to IPC::Open3
#
# $Id: Simple.pm,v 1.7 2006/07/20 13:30:02 erwan Exp $
#
# 060714 erwan Created
#
#################################################################
use strict;
use warnings;
package IPC::Open3::Simple;
use Carp qw(croak confess);
use IPC::Open3;
use IO::Select;
use IO::Handle;
use Data::Dumper;
our $VERSION = '0.04';
#-----------------------------------------------------------------
#
# new - constructor. takes a hash where keys are in, out and err
# and values are closures/coderefs
#
sub new {
my($pkg,%args) = @_;
$pkg = ref $pkg || $pkg;
my $self = bless({},$pkg);
foreach my $type ('in','out','err') {
if (exists $args{$type}) {
croak "".__PACKAGE__."::new expects coderefs" if (ref $args{$type} ne 'CODE');
$self->{$type} = $args{$type};
}
}
return $self;
}
#-----------------------------------------------------------------
#
# run - execute a list of shell commands in a separate process
# and redirect input/output to the closures provided to new()
#
sub run {
my($self,@args) = @_;
# note: in theory, it should work to write:
# my $pid = open3($child_in, $child_out, $child_err, @arguments)
# but that does not work (bug?). $child_err is then undefined
# (in perldoc for open2, the explanation is that stderr=stdout if $child_out == $child_err, which they do when they are both undefined)
# TODO: support interactive ipc with child process?
my $pid = open3(\*CHILD_IN, \*CHILD_OUT, \*CHILD_ERR, @args)
or confess "ERROR: failed to execute command [".join(" ",@args)."]";
my $reader = IO::Select->new();
my $child_in = \*CHILD_IN;
my $child_out = \*CHILD_OUT;
my $child_err = \*CHILD_ERR;
# $child_in->autoflush; IPC::Open3 does it already
$child_out->autoflush;
$child_err->autoflush;
# listen to stdout and stderr, or close them
if (exists $self->{out}) {
$reader->add($child_out);
} else {
$child_out->close();
}
if (exists $self->{err}) {
$reader->add($child_err);
} else {
$child_err->close();
}
# forward stdin to provided function, or close it
if (exists $self->{in}) {
&{$self->{in}}($child_in);
} else {
$child_in->close();
}
# parse output of cvs command
if ($reader->handles) {
while (my @ready = $reader->can_read()) {
foreach my $fh (@ready) {
my $line = <$fh>;
if (!defined $line) {
# reached EOF on this filehandle
$reader->remove($fh);
$fh->close();
} else {
chomp $line;
if ($child_out->opened && fileno($fh) == fileno(\*CHILD_OUT)) {
&{$self->{out}}($line);
} elsif ($child_err->opened && fileno($fh) == fileno(\*CHILD_ERR)) {
&{$self->{err}}($line);
} else {
confess "BUG: got an unexpected filehandle:".Dumper($fh);
}
}
}
}
}
# wait for child process to die
waitpid($pid, 0);
return $self;
}
1;
__END__
=head1 NAME
IPC::Open3::Simple - A simple alternative to IPC::Open3
=head1 VERSION
$Id: Simple.pm,v 1.7 2006/07/20 13:30:02 erwan Exp $
=head1 SYNOPSIS
To run 'ls' in a few directories and put the returned lines in a list:
my @files;
my $ipc = IPC::Open3::Simple->new(out => sub { push @files, $_[0]; })
$ipc->run('ls /etc/');
$ipc->run('ls /home/erwan/');
To run a 'cvs up' and do different stuff with what cvs writes to stdout
and stderr:
IPC::Open3::Simple->new(out => \&parse_cvs_stdout, err => \&parse_cvs_stderr)->run('cvs up');
=head1 DESCRIPTION
IPC::Open3::Simple aims at making it very easy to start a shell command, eventually
feed its stdin with some data, then retrieve its stdout and stderr separately.
When you want to run a shell command and parse its stdout/stderr or feed its
stdin, you often end up using IPC::Run, IPC::Cmd or IPC::Open3 with your
own parsing code, and end up writing more code than you intended.
IPC::Open3::Simple is about removing this overhead and making IPC::Open3
easier to use.
IPC::Open3::Simple calls IPC::Open3 and redirects stdin, stdout
and stderr to some function references passed in argument to the constructor.
It does a select on the input/output filehandles returned by IPC::Open3
and dispatches their content to and from those functions.
=head1 INTERFACE
=over
=item my $runner = IPC::Open3::Simple->B<new>(in => \&sub_in, out => \&sub_out, err => \&sub_err)
Return an object that run commands. Takes no arguments or a hash containing one or
more of the keys 'in', 'out' and 'err'. The values of those keys are function
references (see method I<run> for details).
=item $runner->B<run>(@cmds)
Execute the shell commands I<@cmds>. I<@cmds> follows the same syntax as the command
arguments of I<open3> from IPC::Open3.
I<run> creates a process that executes thoses commands, and connects the process's
stdin, stdout and stderr to the functions passed in the constructor:
If I<out> was defined in I<new>, every line coming from the process's stdout is passed
as first argument to the function reference I<sub_out>. The line is chomped.
If I<err> was defined, the same applies, with lines from the process's stderr being
passed to I<sub_err>.
If I<in> was defined, I<sub_in> is called with a filehandle as first argument.
Everything written to this filehandle will be sent forward to the process's stdin.
I<sub_in> is responsible for calling I<close>() on the filehandle.
I<run> returns only when the command has finished to run.
=back
=head1 DIAGNOSTICS
=over
=item "IPC::Open3::Simple::new expects coderefs"
You called I<new> with 'in', 'err' or 'out' arguments that are not function references.
=item "ERROR: failed to execute command..."
Open3 failed to run the command passed in I<@cmds> to I<run>.
=back
=head1 BUGS AND LIMITATIONS
No bugs so far.
Limitation: IPC::Open3::Simple is not designed for interactive interprocess communication.
Do not use it to steer the process opened by I<open3> via stdin/stdout/stderr, use fork and pipes
or some appropriate IPC module for that. IPC::Open3::Simple's scope is to easily run a command,
eventually with some stdin input, and get its stdout and stderr along the way, not to interactively
communicate with the command.
=head1 SEE ALSO
See IPC::Open3, IPC::Run, IPC::Cmd.
=head1 COPYRIGHT AND LICENSE
Copyright (C) by Erwan Lemonnier C<< <erwan@cpan.org> >>
This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>.
=cut