File Coverage

blib/lib/System/Wrapper.pm
Criterion Covered Total %
statement 21 252 8.3
branch 0 142 0.0
condition 0 50 0.0
subroutine 7 48 14.5
pod 1 16 6.2
total 29 508 5.7


line stmt bran cond sub pod time code
1             package System::Wrapper;
2              
3 1     1   20982 use warnings;
  1         3  
  1         34  
4 1     1   6 use strict;
  1         1  
  1         45  
5 1     1   1403 use overload q{""} => \&stringify;
  1         864  
  1         6  
6 1     1   47 use constant MAX_RAND => 2 ** 32;
  1         1  
  1         58  
7              
8 1     1   5 use Carp;
  1         3  
  1         115  
9 1     1   6 use File::Spec;
  1         1  
  1         1873  
10              
11             our $VERSION = '0.0.2';
12              
13             ## CLASS METHODS
14             sub new {
15 0     0 1   my ( $class, %args ) = @_;
16 0           my $self = bless( { map { $_ => undef }
  0            
17             qw/
18             interpreter executable arguments input output
19             description path order
20             progress spec capture
21             _interpreter _executable _arguments
22             _input _output _destroy
23             _fifo _tmp _input_type _output_type
24             debug verbose
25             /
26             }, $class);
27              
28 0           return $self->_init(%args);
29             }
30              
31             ## OBJECT METHODS
32 0     0     sub _interpreter : lvalue { shift->{_interpreter} }
33 0     0     sub _executable : lvalue { shift->{_executable} }
34 0     0     sub _input : lvalue { shift->{_input} }
35 0     0     sub _output : lvalue { shift->{_output} }
36 0     0     sub _arguments : lvalue { shift->{_arguments} }
37 0     0     sub _fifo : lvalue { shift->{_fifo} }
38 0     0     sub _tmp : lvalue { shift->{_tmp} }
39 0     0     sub _destroy : lvalue { shift->{_destroy} }
40 0     0     sub _input_type : lvalue { shift->{_input_type} }
41 0     0     sub _output_type : lvalue { shift->{_output_type} }
42 0     0 0   sub verbose : lvalue { shift->{verbose} }
43 0     0 0   sub debug : lvalue { shift->{debug} }
44              
45             sub interpreter {
46 0     0 0   my ( $self, $interpreter ) = @_;
47              
48 0 0         if ( defined $interpreter ) {
49 0           $self->{interpreter} = $interpreter;
50             }
51              
52 0           $self->_interpreter = $self->_program_in_path( $self->{interpreter}, 1 );
53              
54 0   0       return $self->_interpreter || $self->{interpreter};
55             }
56              
57             sub executable {
58 0     0 0   my ( $self, $executable ) = @_;
59              
60 0 0         if ( defined $executable ) {
61 0           $self->{executable} = $executable;
62             }
63              
64 0           $self->_executable = $self->_program_in_path( $self->{executable} );
65              
66 0   0       return $self->_executable || $self->{executable};
67             }
68              
69             sub arguments {
70 0     0 0   my ( $self, $args ) = @_;
71              
72 0 0         if ( defined $args ) {
73 0           $self->{arguments} = $args;
74 0           $self->_arguments = $self->_flatten( $self->{arguments} )
75             }
76              
77 0 0 0       return wantarray ? @{ $self->{arguments} ||= [] } : $self->_arguments;
  0            
78             }
79              
80             sub input {
81 0     0 0   my ( $self, $input ) = @_;
82 0           return $self->_io_spec( $input, 'input' );
83             }
84              
85             sub output {
86 0     0 0   my ( $self, $output ) = @_;
87 0           return $self->_io_spec( $output, 'output' );
88             }
89              
90             sub _io_spec {
91 0     0     my ($self, $spec, $io) = @_;
92              
93 0           my $private_method = "_$io";
94              
95 0 0         if ( defined $spec ) {
96              
97 0           my $io_type = $private_method . '_type';
98 0   0       $self->$io_type = ref $spec || 'SCALAR';
99 0           $self->{$io} = $spec;
100 0           $self->$private_method = $self->_flatten( $self->{$io} );
101             }
102              
103 0 0         return wantarray ? $self->_deref( $self->{$io} ) : $self->$private_method;
104             }
105              
106             sub path {
107 0     0 0   my ( $self, $path ) = @_;
108              
109 0 0         if ( defined $path ) {
110 0 0         $path = [$path] if 'SCALAR' eq ref \$path;
111              
112 0           for (@$path) {
113 0 0         _err(
114             "type of arg 1 to 'path' must be directory or reference to array of directories (not non-directory '%s')",
115             $_
116             ) unless -d $_;
117             }
118              
119 0           $self->{path} = $path;
120             }
121 0 0 0       return wantarray ? @{ $self->{path} ||= [] } : $self->{path};
  0            
122             }
123              
124             sub capture {
125 0     0 0   my ( $self, $capture ) = @_;
126              
127 0 0         if ($capture) {
128 0           $self->{capture} = $capture;
129             }
130 0           return $self->{capture};
131             }
132              
133             sub description {
134 0     0 0   my ( $self, $description ) = @_;
135              
136 0 0         if ($description) {
137 0           $self->{description} = $description;
138             }
139 0           return $self->{description};
140             }
141              
142             sub progress {
143 0     0 0   my ( $self, $progress ) = @_;
144              
145 0 0         if ($progress) {
146 0           $self->{progress} = $progress;
147             }
148 0           return $self->{progress};
149             }
150              
151             sub order {
152 0     0 0   my ( $self, $order ) = @_;
153              
154 0 0         if ( defined $order ) {
155 0 0         if ( 'ARRAY' eq ref $order ) {
156 0 0         if (@$order) {
157 0           _err(
158             "parameter 1 to 'order' must have at most %s elements, not %s",
159 0           scalar @{ $self->{order} },
160             scalar @$order
161 0 0         ) if scalar @{ $self->{order} } < scalar @$order;
162              
163 0           my %spec = map { $_ => 1 } @{ $self->{order} };
  0            
  0            
164              
165 0           for ( 0 .. @{ $self->{order} } - 1 ) {
  0            
166 0 0         _err(
167             "parameter 1 values to 'order' must be any of [%s], not '%s'",
168             join( q{, }, sort keys %spec ),
169             $order->[$_]
170             ) unless exists $spec{ $order->[$_] };
171             }
172 0           $self->{order} = $order;
173             }
174             else {
175             }
176             }
177             else {
178 0   0       _err( "type of parameter 1 to 'order' must be ARRAY, not %s",
179             ref $order || ref \$order );
180             }
181             }
182 0 0         return wantarray ? @{ $self->{order} } : $self->{order};
  0            
183             }
184              
185             sub command {
186 0     0 0   my ($self) = @_;
187              
188 0           my @command = grep {$_}
  0            
189 0           map { scalar $self->$_ } $self->order;
190              
191 0 0         return wantarray ? @command : "@command";
192             }
193              
194             sub stringify {
195 0     0 0   my ($self) = @_;
196              
197 0 0         my $out = $self->description ? q{# } . $self->description . qq{\n}
198             : q{}
199             . $self->command . qq{\n};
200              
201 0           return $out;
202             }
203              
204             sub run {
205 0     0 0   my ($self) = @_;
206              
207 0 0         if ($self->debug) {
208 0           $self->_rename_to_tmp;
209 0           print STDERR $self->stringify;
210 0           $self->_rename_from_tmp;
211 0           return;
212             }
213              
214 0 0         if ($self->verbose) {
215 0           print STDERR $self->stringify;
216             }
217              
218 0           $self->_can_run;
219              
220 0           $self->_rename_to_tmp;
221              
222 0           my @command = $self->command;
223              
224 0           my $stdout;
225 0 0         eval { $stdout = $self->capture ? qx/"@command"/ : system "@command" };
  0            
226              
227 0 0         $self->_did_run( $@, $? ) or return;
228              
229 0           $self->_rename_from_tmp;
230              
231 0           $self->_destroy = 1;
232              
233 0           return $stdout;
234             }
235              
236             ## INTERNAL METHODS
237             sub _init {
238 0     0     my ( $self, %args ) = @_;
239              
240 0           $self->{order} = [qw/interpreter executable arguments input output/];
241 0           $self->{path} = [ grep {$_} File::Spec->path, q{.} ];
  0            
242 0           $self->{_tmp} = int rand MAX_RAND;
243              
244 0           while ( my ( $key, $value ) = each %args ) {
245              
246 0 0 0       _err( "Can't access '%s' field in class '%s'",
247             $key, ref $self )
248             if !exists $self->{$key}
249             or $key =~ m/^_/;
250              
251 0           $self->$key($value);
252             }
253              
254 0           return $self;
255             }
256              
257             sub _progress {
258 0     0     my ($self) = @_;
259              
260 0 0         return unless $self->progress;
261              
262 0 0         _err("need input to be set to track progress")
263             unless $self->input;
264              
265 0           my $input_size = 0;
266              
267 0           $input_size += -s $_ for $self->input;
268             }
269              
270             sub _tmp_name {
271 0     0     my ($self, $name) = @_;
272 0           return $name . '.part' . $self->_tmp;
273             }
274              
275             sub _rename_to_tmp {
276 0     0     my ($self) = @_;
277              
278 0 0         return unless $self->output;
279              
280 0 0         if ('HASH' eq $self->_output_type) {
    0          
    0          
281 0           my %output = $self->output;
282             NAME:
283 0           for my $spec ( keys %output ) {
284              
285 1     1   1147 use Data::Dumper;
  1         6765  
  1         1824  
286 0 0         print STDERR Dumper \%output if not defined $output{$spec};
287              
288 0 0         next NAME if -p $output{$spec};
289 0           my $tmp = $self->_tmp_name($output{$spec});
290              
291 0           $output{$spec} = $tmp;
292             }
293 0           $self->output(\%output);
294             }
295             elsif ('ARRAY' eq $self->_output_type) {
296 0           my @output = $self->output;
297             NAME:
298 0           for my $name (@output) {
299 0 0         next NAME if -p $name;
300 0           my $tmp = $self->_tmp_name($name);
301              
302 0           $name = $tmp;
303             }
304 0           $self->output(\@output);
305             }
306             elsif ('SCALAR' eq $self->_output_type) {
307 0           my ($name) = $self->output;
308 0           my $tmp = $self->_tmp_name($name);
309 0           $self->output($tmp);
310             }
311             else {
312 0           _err(
313             "%s is not an accepted output type. It should be impossible to get this error",
314             $self->_output_type
315             );
316             }
317             }
318              
319             sub _rename_from_tmp {
320 0     0     my ($self) = @_;
321              
322 0 0         return unless $self->output;
323              
324 0           my (@name, @tmp);
325              
326 0 0         if ('HASH' eq $self->_output_type) {
    0          
    0          
327 0           my %output = $self->output;
328             NAME:
329 0           for my $spec ( keys %output ) {
330 0           push @tmp, $output{$spec};
331 0           push @name, $output{$spec};
332 0           $name[-1] =~ s/\.part.+//;
333             }
334             }
335             elsif ('ARRAY' eq $self->_output_type) {
336 0           my @output = $self->output;
337             NAME:
338 0           for my $tmp (@output) {
339 0           push @tmp, $tmp;
340 0           push @name, $tmp;
341 0           $name[-1] =~ s/\.part.+//;
342             }
343             }
344             elsif ('SCALAR' eq $self->_output_type) {
345 0           push @tmp, $self->output;
346 0           push @name, $tmp[-1];
347 0           $name[-1] =~ s/\.part.+//;
348             }
349             else {
350 0           _err(
351             "%s is not an accepted output type. It should be impossible to get this error",
352             $self->_output_type
353             );
354             }
355              
356             NAME:
357 0           for (0 .. @name - 1) {
358 0           my ($name, $tmp) = ($name[$_], $tmp[$_]);
359 0 0         next NAME if -p $name;
360              
361 0           my $mv = System::Wrapper->new(
362             interpreter => 'perl',
363             arguments => qq{-e 'rename "$tmp", "$name" or die "\$!"'},
364             );
365 0 0         $mv->verbose = 1 if $self->verbose;
366 0 0         $mv->debug = 1 if $self->debug;
367 0           $mv->run;
368             }
369             }
370              
371             sub _can_run {
372 0     0     my ($self) = @_;
373              
374 0 0         if ( my $nonexistent_file = $self->_inputs_not_available ) {
375 0 0         _err( "%s %s", $nonexistent_file,
376             -e $nonexistent_file
377             ? 'is not readable'
378             : 'does not exist' );
379             }
380              
381 0 0 0       _err("need interpreter or executable to be set to run")
382             unless $self->interpreter
383             or $self->executable;
384              
385 0 0 0       _err( "need interpreter '%s' or executable '%s' to be in path to run",
386             $self->interpreter, $self->executable )
387             unless $self->_interpreter
388             or $self->_executable;
389              
390 0           return 1;
391             }
392              
393             sub _did_run {
394 0     0     my ( $self, $eval_error, $child_error ) = @_;
395              
396 0 0         _err( "failed to run command:\n%s\n%s",
397             scalar $self->command, $eval_error )
398             if $eval_error;
399              
400 0 0         _err(
    0          
401             "failed to run command (%s):\n%s",
402             ( $child_error & 255 )
403             ? 'signal ' . ( $child_error & 255 )
404             : 'exit ' . ( $child_error >> 8 ),
405             scalar $self->command
406             ) if $child_error;
407              
408 0           return 1;
409             }
410              
411             sub _inputs_not_available {
412 0     0     my ($self) = @_;
413              
414 0           for ( $self->input ) {
415 0 0         return $_ unless -r $_;
416             }
417              
418 0           return;
419             }
420              
421             sub _program_in_path {
422 0     0     my ( $self, $program, $is_executable ) = @_;
423              
424 0 0         return unless $program;
425              
426 0           my ( $vol, $dir, $file ) = File::Spec->splitpath($program);
427              
428 0 0 0       return $program
      0        
      0        
      0        
429             if $dir
430             and ( -f $program or -l $program )
431             and ( not defined $is_executable or -x $program );
432              
433 0           for ( @{ $self->path } ) {
  0            
434 0           my $path = File::Spec->catfile( $_, $program );
435              
436 0 0 0       return $path
      0        
      0        
437             if ( -f $path or -l $path )
438             and ( not defined $is_executable or -x $path );
439             }
440              
441 0           return q{};
442             }
443              
444             sub _deref {
445 0     0     my ($self, $struct) = @_;
446              
447 0 0         return unless $struct;
448 0 0         return $struct unless ref $struct;
449 0 0         return $$struct if 'SCALAR' eq ref $struct;
450 0 0         return @$struct if 'ARRAY' eq ref $struct;
451 0 0         return %$struct if 'HASH' eq ref $struct;
452             }
453              
454             sub _flatten {
455 0     0     my ( $self, $struct ) = @_;
456              
457 0 0         return $struct unless ref $struct;
458              
459 0           eval {
460 0           require Storable;
461 0           Storable->import();
462             };
463 0 0         _err("Storable module required: $@") if $@;
464              
465 0           $struct = Storable::dclone($struct);
466              
467 0           my @expanded;
468 0           while ( ref $struct ) {
469              
470 0 0         if ( 'ARRAY' eq ref $struct ) {
    0          
    0          
471 0 0         last unless @$struct;
472 0           push @expanded, $self->_flatten( shift @$struct );
473             }
474              
475             elsif ( 'HASH' eq ref $struct ) {
476 0 0         last unless %$struct;
477 0           my ( $key, $value ) = each %$struct;
478 0 0 0       return unless defined $key and defined $value;
479 0           push @expanded, $key, $self->_flatten($value);
480 0           delete $struct->{$key};
481             }
482              
483             elsif ( 'SCALAR' eq ref $struct ) {
484 0 0         last unless $$struct;
485 0           push @expanded, $self->_flatten($$struct);
486 0           $struct = undef;
487             }
488             else {
489 0           _err(
490             "type of arg 1 to '_flatten' must be a scalar, hash or array reference (not '%s')",
491             ref $struct
492             );
493             }
494             }
495 0           @expanded = grep $_, @expanded;
496 0 0         return wantarray ? @expanded : "@expanded";
497             }
498              
499             sub DESTROY {
500 0     0     my ($self) = @_;
501              
502 0 0         return unless $self->_destroy;
503              
504 0 0         if ( $self->_tmp ) {
505 0           my ( undef, $out_dir, undef )
506             = File::Spec->splitpath( $self->output );
507 0           my $out_files = File::Spec->catfile( $out_dir, q{*} . $self->_tmp );
508              
509 0           unlink glob $out_files;
510             }
511              
512 0 0         if ( $self->_fifo ) {
513 0           unlink glob $self->_fifo;
514             }
515             }
516              
517             ## INTERNAL SUBROUTINES
518             sub _this_sub_name {
519 0   0 0     return ( caller( shift || 1 ) )[3];
520             }
521              
522             sub _err {
523 0     0     my ( $spec, @args ) = @_;
524 0           croak sprintf "%s error: $spec", _this_sub_name(2), @args;
525             }
526              
527              
528             1; # Magic true value required at end of module
529              
530              
531             =head1 NAME
532              
533             System::Wrapper - Class-wrapped system calls and qx operator
534              
535             =head1 VERSION
536              
537             This document describes System::Wrapper version 0.0.2
538              
539              
540             =head1 SYNOPSIS
541              
542             use System::Wrapper;
543              
544             my $command = System::Wrapper->new();
545              
546             $command->interpreter( 'perl');
547             $command->executable( 'program.pl');
548             $command->arguments( [ 'first', {second_a => 2, second_b => 2}, {third => [1,2,3]} ] );
549             $command->input( \@ARGV );
550             $command->output( { '--output' => 'file'}, q{>} => 'file2' );
551             $command->path( [$command->path, q{.}] );
552             $command->capture = 1;
553             $command->verbose = 1;
554             print $command->command;
555             $command->run;
556              
557              
558             =head1 DESCRIPTION
559              
560             This module wraps perl's C<system> call and c<qx> operator in an object-oriented
561             interface. It provides utility methods for accomplishing things that are not very
562             simple in C<system> and C<qx>. This includes in-situ I/O and call success via
563             temporary filenames, C<system> call progress estimation, finding whether the
564             executable and-or interpreter are on the path, validating filenames, cross-platform
565             output operators and argument type specification.
566              
567             This module can be used as a generic wrapper around C<system> and C<qx>, or as
568             a base class for building interfaces to utilities not available to C<perl> itself.
569              
570             =head1 INTERFACE
571              
572             =head2 CLASS METHODS
573              
574             =over
575              
576             =item new(%args)
577              
578             my %args = (
579             interpreter => undef, # optional: string
580             executable => undef, # required: string
581             arguments => undef, # optional: any nested structure of hashes,
582             # arrays or scalar references
583             input => undef, # optional: scalar or array reference
584             output => undef, # optional: hash reference of form { spec => file }
585             # eg: { '>' => 'out' } or { '--output' => 'out' }
586             capture => undef, # optional: return stdout, instead of exit code,
587             # via $self->run
588             path => [ grep $_, File::Spec->path, q{.} ]
589             # required: path of directories on which to look for
590             # interpreter and executable programs
591             );
592              
593             my $command = System::Wrapper->new(%args);
594              
595             =back
596              
597              
598             =head2 SELECTOR METHODS
599              
600             new
601             interpreter
602             executable
603             arguments
604             input
605             output
606             path
607             capture
608             command
609             run
610              
611             =head1 INSTALLATION
612              
613             To install this module type the following:
614              
615             perl Build.PL
616             Build
617             Build test
618             Build install
619              
620             or
621              
622             perl Makefile.PL
623             make
624             make test
625             make install
626              
627              
628             =head1 CONFIGURATION AND ENVIRONMENT
629              
630             System::Wrapper requires no configuration files or environment variables.
631              
632              
633             =head1 INCOMPATIBILITIES
634              
635             None reported.
636              
637              
638             =head1 BUGS AND LIMITATIONS
639              
640             No bugs have been reported.
641              
642             Please report any bugs or feature requests to
643             C<bug-system-wrapper@rt.cpan.org>, or through the web interface at
644             L<http://rt.cpan.org>.
645              
646             =head1 AUTHOR
647              
648             Pedro Silva C<< <psilva+pause@pedrosilva.pt> >>
649              
650              
651             =head1 LICENCE AND COPYRIGHT
652              
653             Copyright (c) 2010, Pedro Silva C<< <psilva+pause@pedrosilva.pt> >>. All rights reserved.
654              
655             This program is free software: you can redistribute it and/or modify
656             it under the terms of the GNU General Public License as published by
657             the Free Software Foundation, either version 3 of the License, or
658             (at your option) any later version.
659              
660             This program is distributed in the hope that it will be useful,
661             but WITHOUT ANY WARRANTY; without even the implied warranty of
662             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
663             GNU General Public License for more details.
664              
665             You should have received a copy of the GNU General Public License
666             along with this program. If not, see L<http://www.gnu.org/licenses/>.