File Coverage

blib/lib/Sys/Cmd.pm
Criterion Covered Total %
statement 258 291 88.6
branch 99 154 64.2
condition 45 88 51.1
subroutine 37 46 80.4
pod 11 14 78.5
total 450 593 75.8


line stmt bran cond sub pod time code
1             package Sys::Cmd;
2 2     2   151282 use v5.18;
  2         8  
3              
4 2     2   10 use warnings;
  2         6  
  2         148  
5 2     2   16 no warnings "experimental::lexical_subs";
  2         4  
  2         94  
6 2     2   14 use feature 'lexical_subs';
  2         6  
  2         438  
7              
8 2     2   14 use Carp qw[];
  2         4  
  2         54  
9 2     2   10 use Cwd qw[getcwd];
  2         2  
  2         148  
10 2     2   12 use Encode::Locale qw[$ENCODING_LOCALE]; # Also Creates the 'locale' alias
  2         4  
  2         264  
11 2     2   14 use Encode qw[encode resolve_alias];
  2         4  
  2         142  
12 2     2   12 use IO::Handle qw[];
  2         4  
  2         64  
13 2     2   1360 use Log::Any qw[$log];
  2         46002  
  2         12  
14 2     2   8080 use Proc::FastSpawn qw[];
  2         1116  
  2         92  
15 2     2   1306 use Sys::Cmd::Process;
  2         6  
  2         704  
16             use Exporter::Tidy _map => {
17 5     5   14973 run => sub { _syscmd( undef, @_ )->_run },
18 33     33   1027025 spawn => sub { _syscmd( undef, @_ )->_spawn },
19 1     1   8076 syscmd => sub { _syscmd( undef, @_ ) },
20             runsub => sub {
21 0     0   0 my $cmd = _syscmd( undef, @_ );
22 0 0   0   0 sub { @_ ? _syscmd( $cmd, @_ )->_run : $cmd->_run }
23 0         0 },
24             spawnsub => sub {
25 0     0   0 my $cmd = syscmd( undef, @_ );
26 0 0   0   0 sub { @_ ? _syscmd( $cmd, @_ )->_spawn : $cmd->_spawn }
27 0         0 },
28 2     2   3138 };
  2         38  
  2         40  
29              
30             our $VERSION = 'v0.986.3';
31              
32             ### START Class::Inline ### v0.0.1 Thu Dec 11 13:24:56 2025
33             require Carp;
34             our ( @_CLASS, $_FIELDS, %_NEW );
35              
36             sub new {
37 35     35 0 85 my $class = shift;
38 35   33     271 my $CLASS = ref $class || $class;
39 35   66     150 $_NEW{$CLASS} //= do {
40 2         52 my ( %seen, @new, @build );
41 2         8 my @possible = ($CLASS);
42 2         8 while (@possible) {
43 2         6 my $c = shift @possible;
44 2     2   492 no strict 'refs';
  2         4  
  2         15416  
45 2 50       6 push @new, $c . '::_NEW' if exists &{ $c . '::_NEW' };
  2         24  
46 2 50       4 push @build, $c . '::BUILD' if exists &{ $c . '::BUILD' };
  2         12  
47 2         6 $seen{$c}++;
48 2 50       10 if ( exists &{ $c . '::DOES' } ) {
  2         14  
49 0         0 push @possible, grep { not $seen{$_}++ } $c->DOES('*');
  0         0  
50             }
51 2         6 push @possible, grep { not $seen{$_}++ } @{ $c . '::ISA' };
  0         0  
  2         46  
52             }
53 2         14 [ [ reverse(@new) ], [ reverse(@build) ] ];
54             };
55 35 50       168 my $self = { @_ ? @_ > 1 ? @_ : %{ $_[0] } : () };
  35 50       168  
56 35         96 bless $self, $CLASS;
57 35         150 my $attrs = { map { ( $_ => 1 ) } keys %$self };
  85         222  
58 35         78 map { $self->$_($attrs) } @{ $_NEW{$CLASS}->[0] };
  35         181  
  35         113  
59             {
60 33         61 local $Carp::CarpLevel = 3;
  33         143  
61 33         117 Carp::carp("Sys::Cmd: unexpected argument '$_'") for keys %$attrs
62             }
63 33         54 map { $self->$_ } @{ $_NEW{$CLASS}->[1] };
  0         0  
  33         83  
64 33         228 $self;
65             }
66              
67             sub _NEW {
68 35     35   80 CORE::state $fix_FIELDS = do {
69 2 50       6 $_FIELDS = { @_CLASS > 1 ? @_CLASS : %{ $_CLASS[0] } };
  2         74  
70 2 50       14 $_FIELDS = $_FIELDS->{'FIELDS'} if exists $_FIELDS->{'FIELDS'};
71             };
72 35 50       80 if ( my @missing = grep { not exists $_[0]->{$_} } 'cmd' ) {
  35         178  
73 0         0 Carp::croak( 'Sys::Cmd required initial argument(s): '
74             . join( ', ', @missing ) );
75             }
76 35         64 $_[0]{'cmd'} = eval { $_FIELDS->{'cmd'}->{'isa'}->( $_[0]{'cmd'} ) };
  35         217  
77 35 50 0     84 delete $_[0]{'cmd'} || Carp::confess( 'Sys::Cmd cmd: ' . $@ ) if $@;
78 11         52 $_[0]{'dir'} = eval { $_FIELDS->{'dir'}->{'isa'}->( $_[0]{'dir'} ) }
79 35 100       110 if exists $_[0]{'dir'};
80 35 100 50     1523 delete $_[0]{'dir'} || Carp::confess( 'Sys::Cmd dir: ' . $@ ) if $@;
81             $_[0]{'encoding'} =
82 0         0 eval { $_FIELDS->{'encoding'}->{'isa'}->( $_[0]{'encoding'} ) }
83 33 50       88 if exists $_[0]{'encoding'};
84 33 50 0     196 delete $_[0]{'encoding'} || Carp::confess( 'Sys::Cmd encoding: ' . $@ )
85             if $@;
86 19         88 $_[0]{'env'} = eval { $_FIELDS->{'env'}->{'isa'}->( $_[0]{'env'} ) }
87 33 100       99 if exists $_[0]{'env'};
88 33 50 0     85 delete $_[0]{'env'} || Carp::confess( 'Sys::Cmd env: ' . $@ ) if $@;
89 0         0 $_[0]{'mock'} = eval { $_FIELDS->{'mock'}->{'isa'}->( $_[0]{'mock'} ) }
90 33 50       132 if exists $_[0]{'mock'};
91 33 50 0     79 delete $_[0]{'mock'} || Carp::confess( 'Sys::Cmd mock: ' . $@ ) if $@;
92 33         84 map { delete $_[1]->{$_} } '_coderef', 'cmd', 'dir', 'encoding', 'env',
  396         765  
93             'err', 'exit', 'input', 'log_any', 'mock', 'on_exit', 'out';
94             }
95              
96             sub __RO {
97 0     0   0 my ( undef, undef, undef, $sub ) = caller(1);
98 0         0 Carp::confess("attribute $sub is read-only");
99             }
100              
101             sub _coderef {
102 32 50   32   92 __RO() if @_ > 1;
103 32   66     221 $_[0]{'_coderef'} //= $_FIELDS->{'_coderef'}->{'default'}->( $_[0] );
104             }
105 98 50 50 98 0 209 sub cmd { __RO() if @_ > 1; $_[0]{'cmd'} // undef }
  98         370  
106 32 50 100 32 1 87 sub dir { __RO() if @_ > 1; $_[0]{'dir'} // undef }
  32         167  
107              
108             sub encoding {
109 65 50   65 1 187 __RO() if @_ > 1;
110 65   66     265 $_[0]{'encoding'} //= eval {
111             $_FIELDS->{'encoding'}->{'isa'}
112 32         297 ->( $_FIELDS->{'encoding'}->{'default'} );
113             };
114 65 50 0     154 delete $_[0]{'encoding'}
115             || Carp::confess( 'invalid (Sys::Cmd::encoding) default: ' . $@ )
116             if $@;
117 65         223 $_[0]{'encoding'};
118             }
119 62 50 100 62 1 160 sub env { __RO() if @_ > 1; $_[0]{'env'} // undef }
  62         485  
120 7 50 100 7 1 23 sub err { __RO() if @_ > 1; $_[0]{'err'} // undef }
  7         49  
121 7 50 100 7 1 25 sub exit { __RO() if @_ > 1; $_[0]{'exit'} // undef }
  7         48  
122 31 50 100 31 1 98 sub input { __RO() if @_ > 1; $_[0]{'input'} // undef }
  31         244  
123 0 0 0 0 0 0 sub log_any { __RO() if @_ > 1; $_[0]{'log_any'} // undef }
  0         0  
124              
125             sub mock {
126 0 0   0 1 0 if ( @_ > 1 ) {
127 0         0 $_[0]{'mock'} = eval { $_FIELDS->{'mock'}->{'isa'}->( $_[1] ) };
  0         0  
128 0 0 0     0 delete $_[0]{'mock'}
129             || Carp::confess( 'invalid (Sys::Cmd::mock) value: ' . $@ )
130             if $@;
131             }
132 0   0     0 $_[0]{'mock'} // undef;
133             }
134              
135             sub on_exit {
136 32 50   32 1 77 if ( @_ > 1 ) { $_[0]{'on_exit'} = $_[1] }
  0         0  
137 32   100     181 $_[0]{'on_exit'} // undef;
138             }
139 7 50 100 7 1 20 sub out { __RO() if @_ > 1; $_[0]{'out'} // undef }
  7         52  
140             @_CLASS = grep 1, ### END Class::Inline ###
141             {
142             cmd => {
143             isa => sub {
144             ref $_[0] eq 'ARRAY' || _croak("cmd must be ARRAYREF");
145             @{ $_[0] } || _croak("Missing cmd elements");
146             if ( grep { !defined $_ } @{ $_[0] } ) {
147             _croak('cmd array cannot contain undef elements');
148             }
149             $_[0];
150             },
151             required => 1,
152             },
153             _coderef => {
154             default => sub {
155             my $c = $_[0]->cmd->[0];
156             ref($c) eq 'CODE' ? $c : undef;
157             },
158             },
159             encoding => {
160             default => $ENCODING_LOCALE,
161             isa => sub {
162             resolve_alias( $_[0] )
163             || _croak("Unknown Encoding: $_[0]");
164             $_[0];
165             },
166             },
167             env => {
168             isa => sub {
169             ref $_[0] eq 'HASH' || _croak("env must be HASHREF");
170             $_[0];
171             },
172             },
173             dir => {
174             isa => sub {
175             -d $_[0] || _croak("directory not found: $_[0]");
176             $_[0];
177             },
178             },
179             input => {},
180             log_any => {},
181             out => {},
182             err => {},
183             exit => {},
184             mock => {
185             is => 'rw',
186             isa => sub {
187             ( ( not defined $_[0] ) || 'CODE' eq ref $_[0] )
188             || _croak('must be CODEref');
189             $_[0];
190             },
191             },
192             on_exit => { is => 'rw', },
193             };
194              
195             sub _croak {
196 9     9   2108 local $Carp::CarpInternal{'Sys::Cmd'} = 1;
197 9         31 local $Carp::CarpInternal{'Sys::Cmd::Process'} = 1;
198 9         3612 Carp::croak(@_);
199             }
200              
201             sub _syscmd {
202 41     41   96 my $template = shift;
203              
204 41         130 my ( @cmd, $opts );
205 41         157 foreach my $arg (@_) {
206 100 100       270 if ( ref($arg) eq 'HASH' ) {
207 28 50       79 _croak( __PACKAGE__ . ': only a single hashref allowed' )
208             if $opts;
209 28         65 $opts = $arg;
210             }
211             else {
212 72         164 push( @cmd, $arg );
213             }
214             }
215 41   100     219 $opts //= {};
216              
217 41 100       120 if ($template) {
218 2         3 $opts->{cmd} = [ @{ $template->cmd }, @cmd ];
  2         6  
219 2 50       6 if ( exists $opts->{env} ) {
220 0         0 my %env = ( each %{ $template->env }, each %{ $opts->{env} } );
  0         0  
  0         0  
221 0         0 $opts->{env} = \%env;
222             }
223 2         10 return Sys::Cmd->new( { %$template, %$opts } );
224             }
225              
226 39 50 33     4260 _croak('$cmd must be defined') unless @cmd && defined $cmd[0];
227              
228 39 100 66     223 if ( 'CODE' ne ref( $cmd[0] ) and not $opts->{mock} ) {
229 37         82 delete $opts->{mock};
230 37         440 require File::Spec;
231 37 100       664 if ( File::Spec->splitdir( $cmd[0] ) == 1 ) {
232 5         1699 require File::Which;
233 5   100     7475 $cmd[0] = File::Which::which( $cmd[0] )
234             || _croak( 'command not found: ' . $cmd[0] );
235             }
236              
237 33 100       1635 if ( !-x $cmd[0] ) {
238 2         14 _croak( 'command not executable: ' . $cmd[0] );
239             }
240             }
241 33         3896 $opts->{cmd} = \@cmd;
242 33         400 Sys::Cmd->new($opts);
243             }
244              
245             my sub _fastspawn {
246 30     30   1039 my @cmd = @_;
247              
248             # Backup the original 0,1,2 file descriptors
249 30         1189 open my $old_fd0, '<&', 0;
250 30         609 open my $old_fd1, '>&', 1;
251 30         388 open my $old_fd2, '>&', 2;
252              
253             # Get new handles to descriptors 0,1,2
254 30         332 my $fd0 = IO::Handle->new_from_fd( 0, 'r' );
255 30         3742 my $fd1 = IO::Handle->new_from_fd( 1, 'w' );
256 30         1688 my $fd2 = IO::Handle->new_from_fd( 2, 'w' );
257              
258             # New handles for the child
259 30         1637 my $stdin = IO::Handle->new;
260 30         627 my $stdout = IO::Handle->new;
261 30         452 my $stderr = IO::Handle->new;
262              
263             # Pipe our filehandles to new child filehandles
264 30 50       1721 pipe( my $child_in, $stdin ) || die "pipe: $!";
265 30 50       744 pipe( $stdout, my $child_out ) || die "pipe: $!";
266 30 50       835 pipe( $stderr, my $child_err ) || die "pipe: $!";
267              
268             # Make sure that 0,1,2 are inherited (probably are anyway)
269 30         307 Proc::FastSpawn::fd_inherit( $_, 1 ) for 0, 1, 2;
270              
271             # But don't inherit the rest
272             Proc::FastSpawn::fd_inherit( fileno($_), 0 )
273 30         511 for $old_fd0, $old_fd1, $old_fd2,
274             $child_in, $child_out, $child_err,
275             $stdin, $stdout, $stderr;
276              
277             # Re-open 0,1,2 by duping the child pipe ends
278 30   50     542 open $fd0, '<&', fileno($child_in) || die "open: $!";
279 30   50     639 open $fd1, '>&', fileno($child_out) || die "open: $!";
280 30   50     415 open $fd2, '>&', fileno($child_err) || die "open: $!";
281              
282             # Kick off the new process
283 30         64 my $pid = eval {
284             Proc::FastSpawn::spawn(
285             $cmd[0],
286             \@cmd,
287             [
288 30 50       291 map { $_ . '=' . ( defined $ENV{$_} ? $ENV{$_} : '' ) }
  858         62746  
289             keys %ENV
290             ]
291             );
292             };
293 30         264 my $err = $@;
294              
295             # Restore our local 0,1,2 to the originals
296 30   50     1046 open $fd0, '<&', fileno($old_fd0) || die "open: $!";
297 30   50     489 open $fd1, '>&', fileno($old_fd1) || die "open: $!";
298 30   50     7300 open $fd2, '>&', fileno($old_fd2) || die "open: $!";
299              
300             # Parent doesn't need to see the child or backup descriptors anymore
301             close($_) || die "close: $!"
302 30   50     599 for $old_fd0,
303             $old_fd1,
304             $old_fd2,
305             $child_in,
306             $child_out,
307             $child_err;
308              
309             # Complain if the spawn failed for some reason
310 30 50       94 _croak($err) if $err;
311 30 50       97 _croak('Unable to spawn child') unless defined $pid;
312              
313             (
314 30         991 pid => $pid,
315             stdin => $stdin,
316             stdout => $stdout,
317             stderr => $stderr
318             );
319             }
320              
321             my sub _fork {
322 2   50 2   12 my $encoding = shift // die 'need encoding';
323 2   50     8 my $code = shift // die 'need code';
324 2         6 my @args = @_;
325 2         28 my ( $stdin, $stdout, $stderr ) =
326             ( IO::Handle->new, IO::Handle->new, IO::Handle->new, );
327              
328 2 50       292 pipe( my $child_in, $stdin ) || die "pipe: $!";
329 2 50       58 pipe( $stdout, my $child_out ) || die "pipe: $!";
330 2 50       48 pipe( $stderr, my $child_err ) || die "pipe: $!";
331              
332 2         20096 my $pid = fork();
333 2 50       333 if ( !defined $pid ) {
334 0         0 my $why = $!;
335 0         0 die "fork: $why";
336             }
337              
338 2 100       146 if ( $pid > 0 ) { # parent
339 1         192 close($_) for
340             $child_in,
341             $child_out,
342             $child_err;
343              
344             return (
345 1         470 pid => $pid,
346             stdin => $stdin,
347             stdout => $stdout,
348             stderr => $stderr
349             );
350             }
351              
352             # Child
353 1         353 $child_err->autoflush(1);
354              
355 1         525 foreach my $quad (
356             [ \*STDERR, '>&=', fileno($child_err), 1 ],
357             [ \*STDOUT, '>&=', fileno($child_out), 1 ],
358             [ \*STDIN, '<&=', fileno($child_in), 0 ],
359             )
360             {
361 3         129 my ( $fh, $mode, $fileno, $autoflush ) = @$quad;
362              
363 3 50       244 open( $fh, $mode, $fileno )
364             or print $child_err sprintf "[%d] open %s, %s: %s\n", $pid,
365             $fh, $mode, $!;
366              
367 3 50       210 binmode $fh, ':encoding(' . $encoding . ')'
368             or warn sprintf "[%d] binmode %d(%s) %s: %s", $pid,
369             $fileno, $mode, $encoding, $!;
370              
371 3 100       536 $fh->autoflush(1) if $autoflush;
372             }
373              
374 1         79 close($_) for
375             $stdin,
376             $stdout,
377             $stderr,
378             $child_in,
379             $child_out,
380             $child_err;
381              
382 1         25 $code->(@args);
383 0         0 _exit(0);
384              
385             # exec( @{ $self->cmd } );
386             # die "exec: $!";
387             }
388              
389             sub _spawn {
390 32     32   85 my $self = shift;
391 32         124 my $dir = $self->dir;
392 32 100       185 my $cwd = getcwd if defined $dir;
393              
394 32         58 my $proc = eval {
395 32         3006 local %ENV = %ENV;
396 32         197 my $locale = $self->encoding;
397 32   100     66 while ( my ( $key, $val ) = each %{ $self->env // {} } ) {
  62         943  
398 30         172 my $keybytes = encode( $locale, $key, Encode::FB_CROAK );
399 30 100       880 if ( defined $val ) {
400 24         90 $ENV{$keybytes} = encode( $locale, $val, Encode::FB_CROAK );
401             }
402             else {
403 6         32 delete $ENV{$keybytes};
404             }
405             }
406              
407 32 100 50     222 chdir $dir or die "chdir: $!" if defined $dir;
408 32         133 my $oe = $self->on_exit;
409              
410             Sys::Cmd::Process->new(
411             cmd => $self->cmd,
412             $oe ? ( on_exit => $oe ) : (),
413             $self->_coderef
414 2         8 ? _fork( $self->encoding, @{ $self->cmd } )
415             : _fastspawn(
416             map {
417 61         1141 encode(
418             $locale => $_,
419             Encode::FB_CROAK | Encode::LEAVE_SRC
420             )
421 32 100       118 } @{ $self->cmd }
  30 100       91  
422             )
423             );
424             };
425 31         147 my $err = $@;
426 31 100       240 chdir $cwd if defined $dir;
427 31 50       103 die $err if $err;
428              
429 31         187 my $enc = ':encoding(' . $self->encoding . ')';
430 2 50   2   2614 binmode( $proc->stdin, $enc ) or warn "binmode stdin: $!";
  2         94  
  2         20  
  31         294  
431 31 50       4960 binmode( $proc->stdout, $enc ) or warn "binmode stdout: $!";
432 31 50       1072 binmode( $proc->stderr, $enc ) or warn "binmode stderr: $!";
433              
434 31         971 $proc->stdin->autoflush(1);
435              
436             # some input was provided
437 31 100       1904 if ( defined( my $input = $self->input ) ) {
438             local $SIG{PIPE} =
439 8     0   202 sub { warn "Broken pipe when writing to:" . $proc->cmdline };
  0         0  
440              
441 8 100 66     136 if ( ( 'ARRAY' eq ref $input ) && @$input ) {
    100          
442 2         16 $proc->stdin->print(@$input);
443             }
444             elsif ( length $input ) {
445 4         10 $proc->stdin->print($input);
446             }
447              
448 8         386 $proc->stdin->close;
449             }
450              
451 31         449 $proc;
452             }
453              
454             sub _run {
455 7     7   15 my $self = shift;
456 7         31 my $proc = $self->_spawn;
457 7         36 my $stderr = $proc->stderr;
458 7         18 my $stdout = $proc->stdout;
459              
460             # Select idea borrowed from System::Command
461 7         1161 require IO::Select;
462 7         2475 my $select = IO::Select->new( $stdout, $stderr );
463 7         639 my @err;
464             my @out;
465              
466 7         28 while ( my @ready = $select->can_read ) {
467 199         2736038 for my $fh (@ready) {
468 206 100       485 my $dest = $fh == $stdout ? \@out : \@err;
469 206 100       1592 if ( defined( my $line = <$fh> ) ) {
470 192         671 push @$dest, $line;
471             }
472             else {
473 14         69 $select->remove($fh);
474 14         662 $fh->close;
475             }
476             }
477             }
478              
479 7         209 $proc->stdin->close;
480 7         120 my $ok = $proc->wait_child;
481              
482 7 100       35 if ( my $ref = $self->exit ) {
    50          
483 3         10 $$ref = $proc->exit;
484             }
485             elsif ( !$ok ) {
486 0         0 _croak( join( '', @err ) . $proc->status );
487             }
488              
489 7 100       34 if ( my $ref = $self->err ) {
    100          
490 3         11 $$ref = join '', @err;
491             }
492             elsif (@err) {
493 1         7 local @Carp::CARP_NOT = (__PACKAGE__);
494 1         253 Carp::carp @err;
495             }
496              
497 7 100       63 if ( my $ref = $self->out ) {
    100          
498 4         56 $$ref = join '', @out;
499             }
500             elsif ( defined( my $wa = wantarray ) ) {
501 2 100       10 return @out if $wa;
502 1         41 return join( '', @out );
503             }
504             }
505              
506             # Legacy object interface, undocumented.
507 2     2 1 769 sub run { _syscmd(@_)->_run }
508 0     0 1   sub spawn { _syscmd(@_)->_spawn }
509              
510             1;
511              
512             __END__