# File: ExecUtils.pm # Project: Anything # Description: Helper functions for executing external programs and scripts from perl. package ExecUtils; use strict; use warnings; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(invoke stringify_arg stringify_args); use Fcntl qw(SEEK_SET); use File::Spec::Functions qw(catfile tmpdir); use File::Temp qw(tempfile); use Time::HiRes qw(gettimeofday tv_interval); # # Run a command and optionally save stderr/stdout # to a variable/file and optionally read stdin # from a variable/file # # Makes use of temporary files to do the reading/writing # from/to variables. # # PROG => program name # BIN => program directory # ARGS => reference to array of program arguments # IN_FILE => file name or handle to set as stdin # IN_VAR => variable (or reference to variable) to feed in as stdin # IN_NAME => the displayed name for the source of stdin # ALL_FILE => file name or handle to store stdout and stderr # ALL_VAR => reference to variable to store stdout and stderr # ALL_NAME => the displayed name for the destination of output # OUT_FILE => file name or handle to store stdout # OUT_VAR => reference to variable to store stdout # OUT_NAME => the displayed name for the destination of stdout # ERR_FILE => file name or handle to store stderr # ERR_VAR => reference to variable to store stderr # ERR_NAME => the displayed name for the destination of stderr # CHECK_STATUS => true to die on bad status codes # TRUNCATE => true to truncate output files if they exist # CMD => reference to store a human readable form of the command run # TIME => reference to store the running time in seconds (floating point) # TMPDIR => directory to create temporary files # sub invoke { my %opts = @_; my $logger = $opts{LOGGER}; $logger->trace("sub invoke") if $logger; # output truncates? my $dir = ($opts{TRUNCATE} ? '>' : '>>'); #direction # temp file directory my $tmpdir = ($opts{TMPDIR} ? $opts{TMPDIR} : &tmpdir()); # get program my $prog = $opts{PROG}; die("No program passed to invoke") unless defined($prog); my $exe = (defined($opts{BIN}) ? &catfile($opts{BIN}, $prog) : $prog); # get args my $args_ref = $opts{ARGS}; my @args = (); if (defined($args_ref)) { @args = @{$args_ref}; } # make command line for printing my $cmd = &stringify_args($prog, @args); # do redirection my $display_name; # check if we're redirecting stdin my ($in_old, $in_tmp, $in_nam); if (defined($opts{IN_FILE}) || defined($opts{IN_VAR})) { $logger->trace("invoke - redirecing stdin") if $logger; # save stdin open($in_old, "<&STDIN") or die("Can't dup STDIN: $!"); # redirect stdin if (defined($opts{IN_FILE})) { # read stdin from specified file if (ref($opts{IN_FILE})) { # file handle (we hope) my $handle = $opts{IN_FILE}; open(STDIN, '<&', $handle) or die("Can't redirect STDIN: $!"); $display_name = 'input_file'; } else { # file name (we hope) my $name = $opts{IN_FILE}; open(STDIN, '<', $name) or die("Can't redirect STDIN: $!"); $display_name = &stringify_arg($name); } } else { # read stdin from a temp file which we preload with the var $in_tmp = &tempfile('stdin_XXXXXXXXXX', DIR => $tmpdir, UNLINK => 1); # make a temporary file my $var = $opts{IN_VAR}; # variable could be passed as a ref or scalar my $var_ref = (ref($var) ? $var : \$var); print $in_tmp ${$var_ref}; # write variable to file seek($in_tmp, 0, SEEK_SET); # rewind file open(STDIN, '<&', $in_tmp) or die("Can't redirect STDIN: $!"); $display_name = '$input'; } $display_name = $opts{IN_NAME} if defined($opts{IN_NAME}); $cmd .= ' < ' . $display_name; } # check for output redirection my ($out_old, $err_old); my ($all_tmp, $out_tmp, $err_tmp); if (defined($opts{ALL_FILE}) || defined($opts{ALL_VAR})) { $logger->trace("invoke - redirecing output") if $logger; # save stdout and stderr open($out_old, ">&STDOUT") or die("Can't dup STDOUT: $!"); open($err_old, ">&STDERR") or die("Can't dup STDERR: $!"); # redirect stdout and stderr if (defined($opts{ALL_FILE})) { # send output to specified file truncate($opts{ALL_FILE}, 0) if ($opts{TRUNCATE}); if (ref($opts{ALL_FILE})) { # file handle (we hope) my $handle = $opts{ALL_FILE}; open(STDOUT, '>>&', $handle) or die("Can't redirect STDOUT: $!"); open(STDERR, '>>&', $handle) or die("Can't redirect STDERR: $!"); $display_name = 'output_file'; } else { # file name (we hope) my $name = $opts{ALL_FILE}; open(STDOUT, '>>', $name) or die("Can't redirect STDOUT: $!"); open(STDERR, '>>', $name) or die("Can't redirect STDERR: $!"); $display_name = &stringify_arg($name); } } else { $all_tmp = &tempfile('allout_XXXXXXXXXX', DIR => $tmpdir, UNLINK => 1); # make a temporary file open(STDOUT, '>>&', $all_tmp) or die("Can't redirect STDOUT: $!"); open(STDERR, '>>&', $all_tmp) or die("Can't redirect STDERR: $!"); $display_name = '$all_messages'; } # turn off buffering so output order is maintained my $oldfh; $oldfh = select(STDOUT); $| = 1; select(STDERR); $| = 1; select($oldfh); # update command $display_name = $opts{ALL_NAME} if defined($opts{ALL_NAME}); $cmd .= ' &'. $dir . ' ' . $display_name; } else { # check if we're redirecting stdout if (defined($opts{OUT_FILE}) || defined($opts{OUT_VAR})) { $logger->trace("invoke - redirecing stdout") if $logger; # save stdout open($out_old, ">&STDOUT") or die("Can't dup STDOUT: $!"); # redirect stdout if (defined($opts{OUT_FILE})) { # send stdout to specified file if (ref($opts{OUT_FILE})) { # file handle (we hope) my $handle = $opts{OUT_FILE}; open(STDOUT, $dir.'&', $handle) or die("Can't redirect STDOUT: $!"); $display_name = 'output_file'; } else { # file name (we hope) my $name = $opts{OUT_FILE}; open(STDOUT, $dir, $name) or die("Can't redirect STDOUT: $!"); $display_name = &stringify_arg($name); } } else { # send stdout to a temp file which we can read in to the var $out_tmp = &tempfile('stdout_XXXXXXXXXX', DIR => $tmpdir, UNLINK => 1); # make a temporary file open(STDOUT, '>&', $out_tmp) or die("Can't redirect STDOUT: $!"); $display_name = '$output_messages'; } $display_name = $opts{OUT_NAME} if defined($opts{OUT_NAME}); $cmd .= ' 1'. $dir . ' ' . $display_name; } # check if we're redirecting stderr if (defined($opts{ERR_FILE}) || defined($opts{ERR_VAR})) { $logger->trace("invoke - redirecing stderr") if $logger; # save stderr open($err_old, ">&STDERR") or die("Can't dup STDERR: $!"); # redirect stderr if (defined($opts{ERR_FILE})) { # send stderr to specified file if (ref($opts{ERR_FILE})) { # file handle (we hope) my $handle = $opts{ERR_FILE}; open(STDERR, $dir.'&', $handle) or die("Can't redirect STDERR: $!"); $display_name = 'error_file'; } else { # file name (we hope) my $name = $opts{ERR_FILE}; open(STDERR, $dir, $name) or die("Can't redirect STDERR: $!"); $display_name = &stringify_arg($name); } } else { # send stderr to a temp file which we can read in to the var $err_tmp = &tempfile('stderr_XXXXXXXXXX', DIR => $tmpdir, UNLINK => 1); # make a temporary file open(STDERR, '>&', $err_tmp) or die("Can't redirect STDERR: $!"); $display_name = '$error_messages'; } $display_name = $opts{ERR_NAME} if defined($opts{ERR_NAME}); $cmd .= ' 2' . $dir . ' ' . $display_name; } } # record the time before starting the program $logger->trace("invoke - recording start time") if $logger; my $t0 = [&gettimeofday()]; # run the command $logger->trace("invoke - running") if $logger; my $status = system($exe, @args); # record the time after completing the program $logger->trace("invoke - recording end time") if $logger; my $t1 = [&gettimeofday()]; # check if the caller wants the elapsed time if (defined($opts{TIME})) { ${$opts{TIME}} = &tv_interval($t0, $t1); } # reset file descriptors if (defined($in_old)) { $logger->trace("invoke - reseting stdin") if $logger; open(STDIN, "<&", $in_old) or die("Can't reset STDIN: $!"); } if (defined($out_old)) { $logger->trace("invoke - reseting stdout") if $logger; open(STDOUT, ">&", $out_old) or die("Can't reset STDOUT: $!"); } if (defined($err_old)) { $logger->trace("invoke - reseting stderr") if $logger; open(STDERR, ">&", $err_old) or die("Can't reset STDERR: $!"); } # close stdin temporary file close($in_tmp) if (defined($in_tmp)); # rewind, slurp and close temporary files ${$opts{ALL_VAR}} = &rewind_slurp_close($all_tmp) if (defined($all_tmp)); ${$opts{OUT_VAR}} = &rewind_slurp_close($out_tmp) if (defined($out_tmp)); ${$opts{ERR_VAR}} = &rewind_slurp_close($err_tmp) if (defined($err_tmp)); if ($opts{CHECK_STATUS}) { $logger->trace("invoke - checking status") if $logger; # check status if ($status == -1) { die("Failed to execute command '". $cmd . "': $!"); } elsif ($status & 127) { die(sprintf("Process executing command '%s' died with signal %d, %s coredump.", $cmd, ($status & 127), ($status & 128) ? 'with' : 'without')); } elsif ($status != 0) { die(sprintf("Process executing command '%s' exited with value %d indicating failure.", $cmd, $? >> 8)); } } if (defined($opts{CMD})) { ${$opts{CMD}} = $cmd; } $logger->trace("invoke - returning") if $logger; return $status; } # # stringify_arg # # Escapes and quotes an argument # sub stringify_arg { my ($argcpy) = @_; # escape shell characters (Bourne shell specific) $argcpy =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g; # quote string if it contains spaces $argcpy = "\"$argcpy\"" if $argcpy =~ m/\s/; return $argcpy; } # # stringify_args # # Convert an arguments array into a string in a way that should # not be ambiguous. Intended for logging. If you are invoking a # program you should still use the extended version of system # that takes an argument array. # sub stringify_args { my @dest = (); foreach my $arg (@_) { push(@dest, &stringify_arg($arg)); } return join(' ', @dest); } sub rewind_slurp_close { my ($fh) = @_; seek($fh, 0, SEEK_SET); my $content = do {local $/ = undef; <$fh>}; close($fh); return $content; }