Group
Extension

Process-SubProcess/bin/run_subprocess.pl

#!/usr/bin/perl

# @author Bodo (Hugo) Barwich
# @version 2023-08-20
# @package Process::SubProcess
# @subpackage bin/run_subprocess.pl

# This Module spawns a sub process from the commandline options and prints the results
# to the STDOUT in a structured parseable output.
#

use strict;
use warnings;

use Getopt::Long::Descriptive;
use Path::Tiny qw(path);
use JSON qw(encode_json);
use YAML qw(Dump);
use Data::Dump qw(dump);

BEGIN {
    use lib "lib";
    use lib "../lib";
}    #BEGIN

use Process::SubProcess;

# ==============================================================================
# Executing Section

=head1 NAME

run_subprocess.pl - Script to run sub processes in an easy post-processable way

=head1 DESCRIPTION

C<run_subprocess.pl> captures the  C<STDOUT>, C<STDERR>, the C<EXIT CODE> from a
sub process and parses it into I<JSON>, I<YAML> or I<Plain Text> formats.

=head1 OVERVIEW

    run_subprocess.pl [-bcdfhnrtx] [long options...]
      -c STR --command STR      the COMMAND to be run
      -n STR --name STR         the NAME for the COMMAND
      -r INT --readtimeout INT  the TIMEOUT for reading of the output from
    			                            COMMAND
      -t INT --timeout INT      the TIMEOUT for execution the COMMAND
      -x --exit                 execution returns exit code
      -f STR --format STR       the format for the output
      -b STR --boundary STR     boundary string for the plain text output
      -d --debug                execution debug output
      -h --help                 print usage message and exit

See L<Method C<Process::SubProcess::setArrProcess()>|Process::SubProcess/"setArrProcess ( CONFIGURATIONS )">

=head1 EXAMPLES

=over 4

=item Plain Test Format with Boundary

    $ bin/run_subprocess.pl -n "test-script fails" -c "t/test_script.pl 3 6" -b ':====' -t 2

    script 'run_subprocess.pl' - Command Result:
    :====SUMMARY:
    command: t/test_script.pl 3 6
    name: test-script fails
    pid: 7387
    exit code: -1
    error code: 4
    :====STDOUT:
    :====STDERR:
    script 'test_script.pl' START 0 ERROR
    Sub Process (7387) 'test-script fails': Execution timed out!
    Execution Time '2 / 2'
    Process will be terminated.
    Sub Process (7387) 'test-script fails': Process terminating ...
    :====END:====

=item JSON Format

    $ bin/run_subprocess.pl -n "test-script" -c "t/test_script.pl 2 0" -f json | jq '.'

    {
      "exit_code": 0,
      "error_code": 0,
      "name": "test-script",
      "command": "t/test_script.pl 2 0",
      "stdout": "Start - Time Now: '1688649512.50548' s\nscript 'test_script.pl' START 0\nscript 'test_script.pl' PAUSE '2' ...\nscript 'test_script.pl' END 1\nEnd - Time Now: '1688649514.50564' s\nscript 'test_script.pl' done in '2000.16093254089' ms\nscript 'test_script.pl' EXIT '0'\n",
      "pid": "14911",
      "stderr": "script 'test_script.pl' START 0 ERROR\nscript 'test_script.pl' END 1 ERROR\n"
    }

=item YAML Format

    $ bin/run_subprocess.pl -n "test-script" -c "t/test_script.pl 2 0" -f yaml

    ---
    command: t/test_script.pl 2 0
    error_code: 0
    exit_code: 0
    name: test-script
    pid: 14928
    stderr: |
      script 'test_script.pl' START 0 ERROR
      script 'test_script.pl' END 1 ERROR
    stdout: |
      Start - Time Now: '1688649560.87845' s
      script 'test_script.pl' START 0
      script 'test_script.pl' PAUSE '2' ...
      script 'test_script.pl' END 1
      End - Time Now: '1688649562.8786' s
      script 'test_script.pl' done in '2000.1528263092' ms
      script 'test_script.pl' EXIT '0'

=item JSON Format extracting C<STDOUT> and C<EXIT CODE>

    $ bin/run_subprocess.pl -n "test-script" -c "t/test_script.pl 2 0" -f json | jq '.stdout,.exit_code'

    "Start - Time Now: '1688649702.23336' s\nscript 'test_script.pl' START 0\nscript 'test_script.pl' PAUSE '2' ...\nscript 'test_script.pl' END 1\nEnd - Time Now: '1688649704.23352' s\nscript 'test_script.pl' done in '2000.15997886658' ms\nscript 'test_script.pl' EXIT '0'\n"
    0

=back

=cut

# ------------------------
# Script Environment

my $module_file = path($0)->basename;
my $path        = Path::Tiny->cwd;
my $maindir     = $path;

# ------------------------
# Script Parameter

my ( $opt, $usage ) = describe_options(
    '%c %o',
    [ 'command|c=s', 'the COMMAND to be run',    { 'required' => 1 } ],
    [ 'name|n=s',    'the NAME for the COMMAND', { 'default'  => '' } ],
    [
        'readtimeout|r=i',
        'the TIMEOUT for reading of the output from COMMAND',
        { 'default' => -1 }
    ],
    [
        'timeout|t=i',
        'the TIMEOUT for execution the COMMAND',
        { 'default' => -1 }
    ],
    [ 'exit|x',     'execution returns exit code', { 'default' => 0 } ],
    [ 'format|f=s', 'the format for the output',   { 'default' => 'plain' } ],
    [
        'boundary|b=s',
        'boundary string for the plain text output',
        { 'default' => '>>>>' }
    ],
    [ 'debug|d', 'execution debug output' ],
    [ 'help|h',  "print usage message and exit", { 'shortcircuit' => 1 } ],
);

if ( $opt->help ) {
    print( $usage->text );

    exit;
}

my %command_res = (
    'command'    => $opt->command,
    'pid'        => -1,
    'name'       => $opt->name,
    'stdout'     => '',
    'stderr'     => '',
    'exit_code'  => -1,
    'error_code' => 0
);

if ( $opt->command ne '' ) {
    my $process =
      Process::SubProcess->new( 'command' => $command_res{'command'} );

    $process->setName( $command_res{'name'} ) if ( $command_res{'name'} ne '' );

    $process->setReadTimeout( $opt->readtimeout )
      if ( $opt->readtimeout != -1 );
    $process->setTimeout( $opt->timeout ) if ( $opt->timeout != -1 );

    $process->Run();

    if ( $opt->debug ) {
        print "proc dmp:\n", dump($process), "\n";
    }

    $command_res{'pid'}        = $process->getProcessID;
    $command_res{'exit_code'}  = $process->getProcessStatus;
    $command_res{'error_code'} = $process->getErrorCode;

    $command_res{'stdout'} = ${ $process->getReportString };
    $command_res{'stderr'} = ${ $process->getErrorString };
}
else {
    $command_res{'error_code'} = 3;
    $command_res{'stderr'} =
      "script '$module_file' - Command Error: Command is missing!";
}

# ------------------------
# Print the Command Result

if ( $opt->format eq 'plain' ) {
    print "script '$module_file' - Command Result:\n";

    printf "%sSUMMARY:\n", $opt->boundary;
    printf "command: %s\nname: %s\npid: %d\nexit code: %d\nerror code: %d\n",
      $command_res{'command'}, $command_res{'name'}, $command_res{'pid'},
      $command_res{'exit_code'}, $command_res{'error_code'};

    printf "%sSTDOUT:\n", $opt->boundary;
    print $command_res{'stdout'};

    printf "%sSTDERR:\n", $opt->boundary;
    print $command_res{'stderr'};

    printf "%sEND%s\n", $opt->boundary, $opt->boundary;
}
elsif ( $opt->format eq 'json' ) {
    print encode_json( \%command_res );
}
elsif ( $opt->format eq 'yaml' ) {
    print Dump( \%command_res );
}
else {
    print "script '$module_file' - Command Result:\n", dump( \%command_res ),
      "\n";
}

if ( $opt->exit ) {
  	if ( $command_res{'exit_code'} > -1 ) {
  		  exit $command_res{'exit_code'};
  	}
  	else {
  		  exit $command_res{'error_code'};
  	}
}


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.