--- /dev/null
+package IPC::Run3;
+
+$VERSION = 0.010;
+
+=head1 NAME
+
+IPC::Run3 - Run a subprocess in batch mode (a la system) on Unix, Win32, etc.
+
+=head1 SYNOPSIS
+
+ use IPC::Run3; ## Exports run3() by default
+ use IPC::Run3 (); ## Don't pollute
+
+ run3 \@cmd, \$in, \$out, \$err;
+ run3 \@cmd, \@in, \&out, \$err;
+
+=head1 DESCRIPTION
+
+This module allows you to run a subprocess and redirect stdin, stdout,
+and/or stderr to files and perl data structures. It aims to satisfy 99%
+of the need for using system()/qx``/open3() with a simple, extremely
+Perlish API and none of the bloat and rarely used features of IPC::Run.
+
+Speed (of Perl code; which is often much slower than the kind of
+buffered I/O that this module uses to spool input to and output from the
+child command), simplicity, and portability are paramount. Disk space
+is not.
+
+Note that passing in \undef explicitly redirects the associated file
+descriptor for STDIN, STDOUT, or STDERR from or to the local equivalent
+of /dev/null (this does I<not> pass a closed filehandle). Passing in
+"undef" (or not passing a redirection) allows the child to inherit the
+corresponding STDIN, STDOUT, or STDERR from the parent.
+
+Because the redirects come last, this allows STDOUT and STDERR to
+default to the parent's by just not specifying them; a common use
+case.
+
+B<Note>: This means that:
+
+ run3 \@cmd, undef, \$out; ## Pass on parent's STDIN
+
+B<does not close the child's STDIN>, it passes on the parent's. Use
+
+ run3 \@cmd, \undef, \$out; ## Close child's STDIN
+
+for that. It's not ideal, but it does work.
+
+If the exact same value is passed for $stdout and $stderr, then
+the child will write both to the same filehandle. In general, this
+means that
+
+ run3 \@cmd, \undef, "foo.txt", "foo.txt";
+ run3 \@cmd, \undef, \$both, \$both;
+
+will DWYM and pass a single file handle to the child for both
+STDOUT and STDERR, collecting all into $both.
+
+=head1 DEBUGGING
+
+To enable debugging use the IPCRUN3DEBUG environment variable to
+a non-zero integer value:
+
+ $ IPCRUN3DEBUG=1 myapp
+
+.
+
+=head1 PROFILING
+
+To enable profiling, set IPCRUN3PROFILE to a number to enable
+emitting profile information to STDERR (1 to get timestamps,
+2 to get a summary report at the END of the program,
+3 to get mini reports after each run) or to a filename to
+emit raw data to a file for later analysis.
+
+=head1 COMPARISON
+
+Here's how it stacks up to existing APIs:
+
+=over
+
+=item compared to system(), qx'', open "...|", open "|...":
+
+=over
+
+=item + redirects more than one file descriptor
+
+=item + returns TRUE on success, FALSE on failure
+
+=item + throws an error if problems occur in the parent process (or the
+pre-exec child)
+
+=item + allows a very perlish interface to perl data structures and
+subroutines
+
+=item + allows 1 word invocations to avoid the shell easily:
+
+ run3 ["foo"]; ## does not invoke shell
+
+=item - does not return the exit code, leaves it in $?
+
+=back
+
+=item compared to open2(), open3():
+
+=over
+
+=item + No lengthy, error prone polling / select loop needed
+
+=item + Hides OS dependancies
+
+=item + Allows SCALAR, ARRAY, and CODE references to source and sink I/O
+
+=item + I/O parameter order is like open3() (not like open2()).
+
+=item - Does not allow interaction with the subprocess
+
+=back
+
+=item compared to IPC::Run::run():
+
+=over
+
+=item + Smaller, lower overhead, simpler, more portable
+
+=item + No select() loop portability issues
+
+=item + Does not fall prey to Perl closure leaks
+
+=item - Does not allow interaction with the subprocess (which
+IPC::Run::run() allows by redirecting subroutines).
+
+=item - Lacks many features of IPC::Run::run() (filters, pipes,
+redirects, pty support).
+
+=back
+
+=back
+
+=cut
+
+@EXPORT = qw( run3 );
+%EXPORT_TAGS = ( all => \@EXPORT );
+@ISA = qw( Exporter );
+use Exporter;
+
+use strict;
+use constant debugging => $ENV{IPCRUN3DEBUG} || $ENV{IPCRUNDEBUG} || 0;
+use constant profiling => $ENV{IPCRUN3PROFILE} || $ENV{IPCRUNPROFILE} || 0;
+use constant is_win32 => 0 <= index $^O, "Win32";
+
+BEGIN {
+ if ( is_win32 ) {
+ eval "use Win32 qw( GetOSName ); 1" or die $@;
+ }
+}
+
+#use constant is_win2k => is_win32 && GetOSName() =~ /Win2000/i;
+#use constant is_winXP => is_win32 && GetOSName() =~ /WinXP/i;
+
+use Carp qw( croak );
+use File::Temp qw( tempfile );
+use UNIVERSAL qw( isa );
+use POSIX qw( dup dup2 );
+
+## We cache the handles of our temp files in order to
+## keep from having to incur the (largish) overhead of File::Temp
+my %fh_cache;
+
+my $profiler;
+
+sub _profiler { $profiler } ## test suite access
+
+BEGIN {
+ if ( profiling ) {
+ eval "use Time::HiRes qw( gettimeofday ); 1" or die $@;
+ if ( $ENV{IPCRUN3PROFILE} =~ /\A\d+\z/ ) {
+ require IPC::Run3::ProfPP;
+ $profiler = IPC::Run3::ProfPP->new(
+ Level => $ENV{IPCRUN3PROFILE},
+ );
+ }
+ else {
+ my ( $dest, undef, $class ) =
+ reverse split /(=)/, $ENV{IPCRUN3PROFILE}, 2;
+ $class = "IPC::Run3::ProfLogger"
+ unless defined $class && length $class;
+ unless ( eval "require $class" ) {
+ my $x = $@;
+ $class = "IPC::Run3::$class";
+ eval "require IPC::Run3::$class" or die $x;
+ }
+ $profiler = $class->new(
+ Destination => $dest,
+ );
+ }
+ $profiler->app_call( [ $0, @ARGV ], scalar gettimeofday() );
+ }
+}
+
+
+END {
+ $profiler->app_exit( scalar gettimeofday() ) if profiling;
+}
+
+
+sub _spool_data_to_child {
+ my ( $type, $source, $binmode_it ) = @_;
+
+ ## If undef (not \undef) passed, they want the child to inherit
+ ## the parent's STDIN.
+ return undef unless defined $source;
+ warn "binmode()ing STDIN\n" if is_win32 && debugging && $binmode_it;
+
+ my $fh;
+ if ( ! $type ) {
+ local *FH; ## Do this the backcompat way
+ open FH, "<$source" or croak "$!: $source";
+ $fh = *FH{IO};
+ if ( is_win32 ) {
+ binmode ":raw"; ## Remove all layers
+ binmode ":crlf" unless $binmode_it;
+ }
+ warn "run3(): feeding file '$source' to child STDIN\n"
+ if debugging >= 2;
+ }
+ elsif ( $type eq "FH" ) {
+ $fh = $source;
+ warn "run3(): feeding filehandle '$source' to child STDIN\n"
+ if debugging >= 2;
+ }
+ else {
+ $fh = $fh_cache{in} ||= tempfile;
+ truncate $fh, 0;
+ seek $fh, 0, 0;
+ if ( is_win32 ) {
+ binmode $fh, ":raw"; ## Remove any previous layers
+ binmode $fh, ":crlf" unless $binmode_it;
+ }
+ my $seekit;
+ if ( $type eq "SCALAR" ) {
+
+ ## When the run3()'s caller asks to feed an empty file
+ ## to the child's stdin, we want to pass a live file
+ ## descriptor to an empty file (like /dev/null) so that
+ ## they don't get surprised by invalid fd errors and get
+ ## normal EOF behaviors.
+ return $fh unless defined $$source; ## \undef passed
+
+ warn "run3(): feeding SCALAR to child STDIN",
+ debugging >= 3
+ ? ( ": '", $$source, "' (", length $$source, " chars)" )
+ : (),
+ "\n"
+ if debugging >= 2;
+
+ $seekit = length $$source;
+ print $fh $$source or die "$! writing to temp file";
+
+ }
+ elsif ( $type eq "ARRAY" ) {
+ warn "run3(): feeding ARRAY to child STDIN",
+ debugging >= 3 ? ( ": '", @$source, "'" ) : (),
+ "\n"
+ if debugging >= 2;
+
+ print $fh @$source or die "$! writing to temp file";
+ $seekit = grep length, @$source;
+ }
+ elsif ( $type eq "CODE" ) {
+ warn "run3(): feeding output of CODE ref '$source' to child STDIN\n"
+ if debugging >= 2;
+ my $parms = []; ## TODO: get these from $options
+ while (1) {
+ my $data = $source->( @$parms );
+ last unless defined $data;
+ print $fh $data or die "$! writing to temp file";
+ $seekit = length $data;
+ }
+ }
+
+ seek $fh, 0, 0 or croak "$! seeking on temp file for child's stdin"
+ if $seekit;
+ }
+
+ croak "run3() can't redirect $type to child stdin"
+ unless defined $fh;
+
+ return $fh;
+}
+
+
+sub _fh_for_child_output {
+ my ( $what, $type, $dest, $binmode_it ) = @_;
+
+ my $fh;
+ if ( $type eq "SCALAR" && $dest == \undef ) {
+ warn "run3(): redirecting child $what to oblivion\n"
+ if debugging >= 2;
+
+ $fh = $fh_cache{nul} ||= do {
+ local *FH;
+ open FH, ">" . File::Spec->devnull;
+ *FH{IO};
+ };
+ }
+ elsif ( !$type ) {
+ warn "run3(): feeding child $what to file '$dest'\n"
+ if debugging >= 2;
+
+ local *FH;
+ open FH, ">$dest" or croak "$!: $dest";
+ $fh = *FH{IO};
+ }
+ else {
+ warn "run3(): capturing child $what\n"
+ if debugging >= 2;
+
+ $fh = $fh_cache{$what} ||= tempfile;
+ seek $fh, 0, 0;
+ truncate $fh, 0;
+ }
+
+ if ( is_win32 ) {
+ warn "binmode()ing $what\n" if debugging && $binmode_it;
+ binmode $fh, ":raw";
+ binmode $fh, ":crlf" unless $binmode_it;
+ }
+ return $fh;
+}
+
+
+sub _read_child_output_fh {
+ my ( $what, $type, $dest, $fh, $options ) = @_;
+
+ return if $type eq "SCALAR" && $dest == \undef;
+
+ seek $fh, 0, 0 or croak "$! seeking on temp file for child $what";
+
+ if ( $type eq "SCALAR" ) {
+ warn "run3(): reading child $what to SCALAR\n"
+ if debugging >= 3;
+
+ ## two read()s are used instead of 1 so that the first will be
+ ## logged even it reads 0 bytes; the second won't.
+ my $count = read $fh, $$dest, 10_000;
+ while (1) {
+ croak "$! reading child $what from temp file"
+ unless defined $count;
+
+ last unless $count;
+
+ warn "run3(): read $count bytes from child $what",
+ debugging >= 3 ? ( ": '", substr( $$dest, -$count ), "'" ) : (),
+ "\n"
+ if debugging >= 2;
+
+ $count = read $fh, $$dest, 10_000, length $$dest;
+ }
+ }
+ elsif ( $type eq "ARRAY" ) {
+ @$dest = <$fh>;
+ if ( debugging >= 2 ) {
+ my $count = 0;
+ $count += length for @$dest;
+ warn
+ "run3(): read ",
+ scalar @$dest,
+ " records, $count bytes from child $what",
+ debugging >= 3 ? ( ": '", @$dest, "'" ) : (),
+ "\n";
+ }
+ }
+ elsif ( $type eq "CODE" ) {
+ warn "run3(): capturing child $what to CODE ref\n"
+ if debugging >= 3;
+
+ local $_;
+ while ( <$fh> ) {
+ warn
+ "run3(): read ",
+ length,
+ " bytes from child $what",
+ debugging >= 3 ? ( ": '", $_, "'" ) : (),
+ "\n"
+ if debugging >= 2;
+
+ $dest->( $_ );
+ }
+ }
+ else {
+ croak "run3() can't redirect child $what to a $type";
+ }
+
+# close $fh;
+}
+
+
+sub _type {
+ my ( $redir ) = @_;
+ return "FH" if isa $redir, "IO::Handle";
+ my $type = ref $redir;
+ return $type eq "GLOB" ? "FH" : $type;
+}
+
+
+sub _max_fd {
+ my $fd = dup(0);
+ POSIX::close $fd;
+ return $fd;
+}
+
+my $run_call_time;
+my $sys_call_time;
+my $sys_exit_time;
+
+sub run3 {
+ $run_call_time = gettimeofday() if profiling;
+
+ my $options = @_ && ref $_[-1] eq "HASH" ? pop : {};
+
+ my ( $cmd, $stdin, $stdout, $stderr ) = @_;
+
+ print STDERR "run3(): running ",
+ join( " ", map "'$_'", ref $cmd ? @$cmd : $cmd ),
+ "\n"
+ if debugging;
+
+ if ( ref $cmd ) {
+ croak "run3(): empty command" unless @$cmd;
+ croak "run3(): undefined command" unless defined $cmd->[0];
+ croak "run3(): command name ('')" unless length $cmd->[0];
+ }
+ else {
+ croak "run3(): missing command" unless @_;
+ croak "run3(): undefined command" unless defined $cmd;
+ croak "run3(): command ('')" unless length $cmd;
+ }
+
+ my $in_type = _type $stdin;
+ my $out_type = _type $stdout;
+ my $err_type = _type $stderr;
+
+ ## This routine procedes in stages so that a failure in an early
+ ## stage prevents later stages from running, and thus from needing
+ ## cleanup.
+
+ my $in_fh = _spool_data_to_child $in_type, $stdin,
+ $options->{binmode_stdin} if defined $stdin;
+
+ my $out_fh = _fh_for_child_output "stdout", $out_type, $stdout,
+ $options->{binmode_stdout} if defined $stdout;
+
+ my $tie_err_to_out =
+ defined $stderr && defined $stdout && $stderr eq $stdout;
+
+ my $err_fh = $tie_err_to_out
+ ? $out_fh
+ : _fh_for_child_output "stderr", $err_type, $stderr,
+ $options->{binmode_stderr} if defined $stderr;
+
+ ## this should make perl close these on exceptions
+ local *STDIN_SAVE;
+ local *STDOUT_SAVE;
+ local *STDERR_SAVE;
+
+ my $saved_fd0 = dup( 0 ) if defined $in_fh;
+
+# open STDIN_SAVE, "<&STDIN"# or croak "run3(): $! saving STDIN"
+# if defined $in_fh;
+ open STDOUT_SAVE, ">&STDOUT" or croak "run3(): $! saving STDOUT"
+ if defined $out_fh;
+ open STDERR_SAVE, ">&STDERR" or croak "run3(): $! saving STDERR"
+ if defined $err_fh;
+
+ my $ok = eval {
+ ## The open() call here seems to not force fd 0 in some cases;
+ ## I ran in to trouble when using this in VCP, not sure why.
+ ## the dup2() seems to work.
+ dup2( fileno $in_fh, 0 )
+# open STDIN, "<&=" . fileno $in_fh
+ or croak "run3(): $! redirecting STDIN"
+ if defined $in_fh;
+
+# close $in_fh or croak "$! closing STDIN temp file"
+# if ref $stdin;
+
+ open STDOUT, ">&" . fileno $out_fh
+ or croak "run3(): $! redirecting STDOUT"
+ if defined $out_fh;
+
+ open STDERR, ">&" . fileno $err_fh
+ or croak "run3(): $! redirecting STDERR"
+ if defined $err_fh;
+
+ $sys_call_time = gettimeofday() if profiling;
+
+ my $r = ref $cmd
+ ? system {$cmd->[0]}
+ is_win32
+ ? map {
+ ## Probably need to offer a win32 escaping
+ ## option, every command may be different.
+ ( my $s = $_ ) =~ s/"/"""/g;
+ $s = qq{"$s"};
+ $s;
+ } @$cmd
+ : @$cmd
+ : system $cmd;
+
+ $sys_exit_time = gettimeofday() if profiling;
+
+ unless ( defined $r ) {
+ if ( debugging ) {
+ my $err_fh = defined $err_fh ? \*STDERR_SAVE : \*STDERR;
+ print $err_fh "run3(): system() error $!\n"
+ }
+ die $!;
+ }
+
+ if ( debugging ) {
+ my $err_fh = defined $err_fh ? \*STDERR_SAVE : \*STDERR;
+ print $err_fh "run3(): \$? is $?\n"
+ }
+ 1;
+ };
+ my $x = $@;
+
+ my @errs;
+
+ if ( defined $saved_fd0 ) {
+ dup2( $saved_fd0, 0 );
+ POSIX::close( $saved_fd0 );
+ }
+
+# open STDIN, "<&STDIN_SAVE"# or push @errs, "run3(): $! restoring STDIN"
+# if defined $in_fh;
+ open STDOUT, ">&STDOUT_SAVE" or push @errs, "run3(): $! restoring STDOUT"
+ if defined $out_fh;
+ open STDERR, ">&STDERR_SAVE" or push @errs, "run3(): $! restoring STDERR"
+ if defined $err_fh;
+
+ croak join ", ", @errs if @errs;
+
+ die $x unless $ok;
+
+ _read_child_output_fh "stdout", $out_type, $stdout, $out_fh, $options
+ if defined $out_fh && $out_type && $out_type ne "FH";
+ _read_child_output_fh "stderr", $err_type, $stderr, $err_fh, $options
+ if defined $err_fh && $err_type && $err_type ne "FH" && !$tie_err_to_out;
+ $profiler->run_exit(
+ $cmd,
+ $run_call_time,
+ $sys_call_time,
+ $sys_exit_time,
+ scalar gettimeofday
+ ) if profiling;
+
+ return 1;
+}
+
+my $in_fh;
+my $in_fd;
+my $out_fh;
+my $out_fd;
+my $err_fh;
+my $err_fd;
+ $in_fh = tempfile;
+ $in_fd = fileno $in_fh;
+ $out_fh = tempfile;
+ $out_fd = fileno $out_fh;
+ $err_fh = tempfile;
+ $err_fd = fileno $err_fh;
+ my $saved_fd0 = dup 0;
+ my $saved_fd1 = dup 1;
+ my $saved_fd2 = dup 2;
+ my $r;
+ my ( $cmd, $stdin, $stdout, $stderr );
+
+sub _run3 {
+ ( $cmd, $stdin, $stdout, $stderr ) = @_;
+
+ truncate $in_fh, 0;
+ seek $in_fh, 0, 0;
+
+ print $in_fh $$stdin or die "$! writing to temp file";
+ seek $in_fh, 0, 0;
+
+ seek $out_fh, 0, 0;
+ truncate $out_fh, 0;
+
+ seek $err_fh, 0, 0;
+ truncate $err_fh, 0;
+
+ dup2 $in_fd, 0 or croak "run3(): $! redirecting STDIN";
+ dup2 $out_fd, 1 or croak "run3(): $! redirecting STDOUT";
+ dup2 $err_fd, 2 or croak "run3(): $! redirecting STDERR";
+
+ $r =
+ system {$cmd->[0]}
+ is_win32
+ ? map {
+ ## Probably need to offer a win32 escaping
+ ## option, every command is different.
+ ( my $s = $_ ) =~ s/"/"""/g;
+ $s = q{"$s"} if /[^\w.:\/\\'-]/;
+ $s;
+ } @$cmd
+ : @$cmd;
+
+ die $! unless defined $r;
+
+ dup2 $saved_fd0, 0;
+ dup2 $saved_fd1, 1;
+ dup2 $saved_fd2, 2;
+
+ seek $out_fh, 0, 0 or croak "$! seeking on temp file for child output";
+
+ my $count = read $out_fh, $$stdout, 10_000;
+ while ( $count == 10_000 ) {
+ $count = read $out_fh, $$stdout, 10_000, length $$stdout;
+ }
+ croak "$! reading child output from temp file"
+ unless defined $count;
+
+ seek $err_fh, 0, 0 or croak "$! seeking on temp file for child errput";
+
+ $count = read $err_fh, $$stderr, 10_000;
+ while ( $count == 10_000 ) {
+ $count = read $err_fh, $$stderr, 10_000, length $$stdout;
+ }
+ croak "$! reading child stderr from temp file"
+ unless defined $count;
+
+ return 1;
+}
+
+=cut
+
+
+=head1 TODO
+
+pty support
+
+=head1 LIMITATIONS
+
+Often uses intermediate files (determined by File::Temp, and thus by the
+File::Spec defaults and the TMPDIR env. variable) for speed, portability and
+simplicity.
+
+=head1 COPYRIGHT
+
+ Copyright 2003, R. Barrie Slaymaker, Jr., All Rights Reserved
+
+=head1 LICENSE
+
+You may use this module under the terms of the BSD, Artistic, or GPL licenses,
+any version.
+
+=head1 AUTHOR
+
+Barrie Slaymaker <barries@slaysys.com>
+
+=cut
+
+1;