NAME

IPC::Open3::Utils - simple API encapsulating the most common open3() logic/uses including handling various corner cases and caveats

VERSION

This document describes IPC::Open3::Utils version 0.92

DESCRIPTION

The goals of this module are:

1 Encapsulate logic done every time you want to use open3().
2 boolean check of command execution
3 Out of the box printing to STDOUT/STDERR or assignments to variables (see #5)
4 open3() error reporting
5 comprehensive but simple output processing handlers for flexibility (see #3)
6 Lightweight utilities for examining the meaning of $? without POSIX

SYNOPSIS

use IPC::Open3::Utils qw(run_cmd put_cmd_in ...);


run_cmd(@cmd); # like 'system(@cmd)'

# like 'if (system(@cmd) != 0)'
if (!run_cmd(@cmd)) {
    print "Oops you may need to re-run that command, it failed.\n1";   
}

So far not too useful but its when you need more complex things than system()-like behavior (and why you are using open3() to begin with one could assume) that this module comes into play.

If you care about exactly what went wrong you can get very detailed:

my $open3_error;
if (!run_cmd(@cmd, {'open3_error' => \$open3_error})) {
    print "open3() said: $open3_error\n" if $open3_error;
    
    if ($!) {
        print int($!) . ": $!\n";
    }
    
    if ($?) { # or if (!child_error_ok($?)) {
    
        print "Command failed to execute.\n" if child_error_failed_to_execute($?);
        print "Command seg faulted.\n" if child_error_seg_faulted($?);
        print "Command core dumped.\n" if child_error_core_dumped($?);
        unless ( child_error_failed_to_execute($?) ) {
            print "Command exited with signal: " . child_error_exit_signal($?) . ".\n";
            print "Command exited with value: " . child_error_exit_value($?) . ".\n";
        }
    }
}

You can slurp the output into variables:

# both STDOUT/STDERR in one
my @output;
if (put_cmd_in(@cmd, \@output)) {
    print _my_stringify(\@output);
}

# seperate STDOUT/STDERR
my @stdout;
my $stderr;
if (put_cmd_in(@cmd, \@stdout, \$stderr)) {
    print "The command ran ok\n";
    print "The output was: " . _my_stringify(\@stdout);
    if ($stderr) {
        print "However there were errors reported:" . _my_stringify($stderr);
    }
}

You can look for a certain piece of data then stop processing once you have it:

my $widget_value;
run_cmd(@cmd, {
   'handler' => sub {
       my ($cur_line, $stdin, $is_stderr, $is_open3_err, $short_circuit_loop_boolean_scalar_ref) = @_;
       
       if ($cur_line =~ m{^\s*widget_value:\s*(\d+)}) {
           $widget_value = $1;
           ${ short_circuit_loop_boolean_scalar_ref } = 1;
       }
       
       return 1;
    },
});

if (defined $widget_value) {
    print "You Widget is set to $widget_value.";
}
else {
    print "You do not have a widget value set.";
} 

You can do any or all of it!

EXPORT

All functions can be exported.

run_cmd() and put_cmd_in() are exported by default and via ':cmd'

:all will export, well, all functions

:err will export all child_error* functions.

INTERFACE

Both of these functions:

  • take an array containing the command to run through open3() as its first arguments

  • take an optional configuration hashref as the last argument (described below in "%args")

  • return true if the command was executed successfully and false otherwise.

run_cmd()

run_cmd(@cmd)
run_cmd(@cmd, \%args)

By default the 'handler' (see "%args" below) prints the command's STDOUT and STDERR to perl's STDOUT and STDERR.

put_cmd_in()

Same %args as run_cmd() but it overrides 'handler' with one that populates the given "output" refs.

You can have one "output" ref to combine the command's STDERR/STDOUT into one variable. Or two, one for STDOUT and one for STDERR.

The ref can be an ARRAY reference or a SCALAR reference and are specified after the command and before the args hashref (if any)

put_cmd_in(@cmd, \@all_output, \%args)
put_cmd_in(@cmd, \$all_output, \%args)
put_cmd_in(@cmd, \@stdout, \@stderr, \%args)
put_cmd_in(@cmd, \$stdout, \$stderr, \%args)

To not waste memory on one that you are not interested in simply pass it undef for the one you don't care about.

put_cmd_in(@cmd, undef, \@stderr, \%args);
put_cmd_in(@cmd, \@stdout, undef, \%args)

Or quiet it up completely.

put_cmd_in(@cmd, undef, undef, \%args)

or progressivley getting simpler:

put_cmd_in(@cmd, undef, \%args);
put_cmd_in(@cmd, \%args);
put_cmd_in(@cmd);

Note that using one "output" ref does not gaurantee the output will be in the same order as it is when you execute the command via the shell due to the handling of the filehandles via IO::Select. Due to that occasionally a test regarding single "output" ref testing will fail. Just run it again and it should be fine :)

%args

This is an optional 'last arg' hashref that configures behavior and functionality of run_cmd() and put_cmd_in()

Below are the keys and a description of their values.

handler

A code reference that should return a boolean status. If it returns false run_cmd() and put_cmd_in() will also return false.

If it returns true and assuming open3() threw no errors that'd make them return false then run_cmd() and put_cmd_in() will return true.

Any exceptions thrown in the handler are caught and put in $@. Then the open3 cleanup happens and the function returns false.

It gets the following arguments sent to it:

1 The current line of the command's output
2 The command's STDIN IO::Handle object
3 A boolean of whether or not the line is from the command's STDERR
4 A boolean of whether or not the line is an error from open3()
5 A scalar ref that when set to true will stop the while loop the command is running in.

This is useful for efficiency so you can stop processing the command once you get what you're interested in and still return true overall.

'handler' => sub {
    my ($cur_line, $stdin, $is_stderr, $is_open3_err, $short_circuit_loop_boolean_scalar_ref) = @_;  
    ...
    return 1;
},
timeout

The number of seconds you want to allow the execution to run. If it takes longer than the specified amount, it sets $@ to "Alarm clock" ($! will probably be 4), does the open3 cleanup, and returns false.

If Time::HiRes's alarm() is available it uses that instead of alarm(). In that case you can set its value to the number of microseconds you want to allow it to run.

run_cmd( @cmd, { 'timeout' => 42 } ); 

run_cmd( @cmd, { 'timeout' => 3.14159 } ); # The '.14159' is sort of pointless unless you've brought in Time::HiRes

Any previous alarm is set back to what it was once it is complete.

All normal alarm/sleep/computer time caveats apply. That includes mixing large normal alarm() w/ HiRes alarms. For example, in the first command below it seems like Time::HiRes's alarm() should be much closer to 10K but it says over 8.5K seconds have elapsed, the second looks like what we expect:

$ perl -mTime::HiRes -le 'print alarm(10_000);print Time::HiRes::alarm(100.1);print alarm(0);'
0
1410.065364
101
$ 

$ perl -mTime::HiRes -le 'print alarm(1_000);print Time::HiRes::alarm(100.1);print alarm(0);'
0
999.999876
101
$
timeout_is_microseconds

If you want to specify a microsecond timeout you can set 'timeout_is_microseconds' to true.

If Time::HiRes's ualarm() is not available the value is turned into seconds and a normal alarm is used. If this happens and the result is under one second then the alarm is set for 1 second.

use Time::HiRes;
run_cmd( 'blink', { 'timeout' => 350_001, 'timeout_is_microseconds' => 1 } );
close_stdin

Boolean to have the command's STDIN closed immediately after the open3() call.

If this is set to true then the stdin variable in your handler's arguments will be undefined.

pre_read_print_to_stdin

The value can be one of three types:

String to pass to the command's stdin via IO::Handle's printflush() method.

Array ref of strings to pass to the command's stdin via IO::Handle's printflush() method.

Code ref that returns one or more strings to pass to the command's stdin via IO::Handle's printflush() method.

ignore_handle

The value of this can be 'stderr' or 'stdout' and will cause the named handle to not even be included in the while() loop and hence never get to the 'handler'.

This might be useful to, say, make run_cmd() only print the command's STDERR.

run_cmd(@cmd); # default handler prints the command's STDERR and STDOUT to perl's STDERR and STDOUT
run_cmd(@cmd, { 'ignore_handle' => 'stdout' }); # only print the command's STDERR to perl's STDERR
run_cmd(@cmd, { 'ignore_handle' => 'stderr' }); # only print the command's STDOUT to perl's STDOUT
autoflush

This is a hashref that tells which, if any, handles you want autoflush turned on for (IE $handle->autoflush(1) See IO::Handle).

It can have 3 keys whose value is a boolean that, when true, will turn on the handle's autoflush before the open3() call.

Those keys are 'stdout', 'stderr', 'sdtin'

run_cmd(@cmd, {
    'autoflush' => {
        'stdout' => 1,
        'stderr' => 1,
        'stdin' => 1, # open3() will probably already have done this but just in case you want to be explicit
    },
});
read_length_bytes

Number of bytes to read from the command via sysread (minimum 128). The default is to use readline()

open3_error

This is the key that any open3() errors get put in for post examination. If it is a SCALAR ref then the error will be in the variable it references.

my %args;
if (!run_cmd(@cmd,\%args)) {
   # $args{'open3_error'} will have the error if it was from open3() 
}

As of verison 0.8 this will typically also be in $@. (See note in TODO)

carp_open3_errors

Boolean to carp() errors from open3() itself. Default is false.

stop_read_on_open3_err

Boolean to quit the loop if an open3() error is thrown. This will more than likley happen anyway, this is just explicit. Default is false.

Child Error Code Exit code utilities

Each of these child_error* functions opertates on the value of $? or the argument you pass it.

child_error_ok()

Returns true if the value indicates success.

if ( child_error_ok(system(@cmd)) ) {
    print "The command was run successfully\n";
}
child_error_failed_to_execute()

Returns true if the value indicates failure to execute.

child_error_seg_faulted()

Returns true if the value indicated that the execution had a segmentaton fault

child_error_core_dumped()

Returns true if the value indicated that the execution had a core dump

child_error_exit_signal()

Returns the exit signal that the value represents

child_error_exit_value()

Returns the exit value that the value represents

DIAGNOSTICS

Throws no warnings or errors of its own. Capturing errors associated with a given command are documented above.

CONFIGURATION AND ENVIRONMENT

IPC::Open3::Utils requires no configuration files or environment variables.

DEPENDENCIES

IPC::Open3, IO::Handle, IO::Select

INCOMPATIBILITIES

None reported.

BUGS AND LIMITATIONS

No bugs have been reported.

Please report any bugs or feature requests to bug-ipc-open3-utils@rt.cpan.org, or through the web interface at http://rt.cpan.org.

TODO

- autoflush() by default ?

- if not closed && !autoflushed() finish read ?

- Add 'blocking' $io->blocking($value) ?

- Add filehandle support to put_cmd_in()

- find out why $! seems to always be 'Bad File Descriptor' on some systems

- no_hires_timeout attribute to forceusing built in alarm() even when Time::HiRes functions are available ?

- drop post-open3() call open3_error logic since it is caught immediately and put in $@ or is it possible it can peter out ambiguously later ?

- open3 eval under alarm

AUTHOR

Daniel Muey <http://drmuey.com/cpan_contact.pl>

LICENCE AND COPYRIGHT

Copyright (c) 2008, Daniel Muey <http://drmuey.com/cpan_contact.pl>. All rights reserved.

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perlartistic.

DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.1