File Coverage

blib/lib/IPC/System/Options.pm
Criterion Covered Total %
statement 165 284 58.1
branch 93 194 47.9
condition 21 43 48.8
subroutine 19 21 90.4
pod 4 5 80.0
total 302 547 55.2


line stmt bran cond sub pod time code
1             package IPC::System::Options;
2              
3 1     1   95300 use strict 'subs', 'vars';
  1         10  
  1         36  
4 1     1   6 use warnings;
  1         1  
  1         28  
5              
6 1     1   434 use Proc::ChildError qw(explain_child_error);
  1         350  
  1         187  
7              
8             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
9             our $DATE = '2023-05-24'; # DATE
10             our $DIST = 'IPC-System-Options'; # DIST
11             our $VERSION = '0.341'; # VERSION
12              
13             my $log;
14             our %Global_Opts;
15              
16             sub import {
17 1     1   10 my $self = shift;
18              
19 1         4 my $caller = caller();
20 1         20 my $i = 0;
21 1         4 while ($i < @_) {
22             # backtick is the older, deprecated name for readpipe
23 4 50       17 if ($_[$i] =~ /\A(system|readpipe|backtick|run|start|import)\z/) {
    0          
24 1     1   23 no strict 'refs';
  1         2  
  1         1224  
25 4         7 *{"$caller\::$_[$i]"} = \&{"$self\::" . $_[$i]};
  4         15  
  4         11  
26             } elsif ($_[$i] =~ /\A-(.+)/) {
27 0 0       0 die "$_[$i] requires an argument" unless $i < @_-1;
28 0         0 $Global_Opts{$1} = $_[$i+1];
29 0         0 $i++;
30             } else {
31 0         0 die "$_[$i] is not exported by ".__PACKAGE__;
32             }
33 4         2215 $i++;
34             }
35             }
36              
37             sub _args2cmd {
38 2 50   2   14 if (@_ == 1) {
39 0         0 return $_[0];
40             }
41 2 50       23 if ($^O eq 'MSWin32') {
42 0         0 require Win32::ShellQuote;
43             return Win32::ShellQuote::quote_system_string(
44 0 0       0 map { ref($_) eq 'SCALAR' ? $$_ : $_ } @_);
  0         0  
45             } else {
46 2         872 require String::ShellQuote;
47             return join(
48             " ",
49 2 100       971 map { ref($_) eq 'SCALAR' ? $$_ : String::ShellQuote::shell_quote($_) } @_
  8         208  
50             );
51             }
52             }
53              
54             sub _system_or_readpipe_or_run_or_start {
55 17     17   45 my $which = shift;
56 17 100       55 my $opts = ref($_[0]) eq 'HASH' ? shift : {};
57 17         55 for (keys %Global_Opts) {
58 0 0       0 $opts->{$_} = $Global_Opts{$_} if !defined($opts->{$_});
59             }
60 17         61 my @args = @_;
61              
62             # check known options
63 17         53 for (keys %$opts) {
64 20 100       233 die "Unknown option '$_'"
65             unless /\A(
66             capture_stdout|capture_stderr|capture_merged|
67             tee_stdout|tee_stderr|tee_merged|
68             chdir|dies?|dry_run|env|lang|log|max_log_output|shell|
69             exit_code_success_criteria|
70             fail_log_level|
71             stdin # XXX: only for run()
72             )\z/x;
73             }
74              
75             # defaults
76 16   50     178 $opts->{fail_log_level} ||= 'error';
77              
78 16   66     127 my $opt_die = $opts->{die} || $opts->{dies};
79              
80 16         35 my $child_error;
81 16         29 my $os_error = "";
82 16         22 my $exit_code_is_success;
83             my $extra_error;
84              
85             my $code_exit_code_is_success = sub {
86 14     14   99 my $exit_code = shift;
87 14 50       112 if (defined $opts->{exit_code_success_criteria}) {
88 0 0       0 if (ref $opts->{exit_code_success_criteria} eq '' ) { return $exit_code == $opts->{exit_code_success_criteria} }
  0 0       0  
    0          
    0          
89 0 0       0 elsif (ref $opts->{exit_code_success_criteria} eq 'ARRAY' ) { return (grep { $exit_code==$_ } @{ $opts->{exit_code_success_criteria} }) ? 1:0 }
  0         0  
  0         0  
90 0         0 elsif (ref $opts->{exit_code_success_criteria} eq 'Regexp') { return $exit_code =~ $opts->{exit_code_success_criteria} }
91 0         0 elsif (ref $opts->{exit_code_success_criteria} eq 'CODE' ) { return $opts->{exit_code_success_criteria}->($exit_code) }
92 0         0 else { die "exit_code_success_criteria must be a number, array of numbers, Regexp, or coderef" }
93             } else {
94 14         237 return $exit_code == 0;
95             }
96 16         142 };
97              
98 16 50       55 if ($opts->{log}) {
99 0         0 require Log::ger::Format::MultilevelLog; # just so scan_prereqs can detect it
100 0         0 require Log::ger::Format;
101 0         0 Log::ger::Format->set_for_current_package(MultilevelLog => (sub_name => 'logger'));
102 0         0 require Log::ger;
103 0         0 Log::ger->import;
104             }
105              
106 16         24 my $cwd;
107 16 100       41 if ($opts->{chdir}) {
108 2         37 require Cwd;
109 2         34 $cwd = Cwd::getcwd();
110 2 50       8 if (!defined $cwd) { # checking $! is always true here, why?
111 0 0       0 $log->error("Can't getcwd: $!") if $log;
112 0         0 $child_error = -1;
113 0         0 $exit_code_is_success = 0;
114 0         0 $os_error = $!;
115 0         0 $extra_error = "Can't getcwd";
116 0         0 goto CHECK_RESULT;
117             }
118 2 100       45 unless (chdir $opts->{chdir}) {
119 1 50       20 $log->error("Can't chdir to '$opts->{chdir}': $!") if $log;
120 1         15 $child_error = -1;
121 1         4 $exit_code_is_success = 0;
122 1         15 $os_error = $!;
123 1         5 $extra_error = "Can't chdir";
124 1         15 goto CHECK_RESULT;
125             }
126             }
127              
128             # set ENV
129 15         27 my %save_env;
130             my %set_env;
131 15 50       36 if ($opts->{lang}) {
132 0         0 $set_env{LC_ALL} = $opts->{lang};
133 0         0 $set_env{LANGUAGE} = $opts->{lang};
134 0         0 $set_env{LANG} = $opts->{lang};
135             }
136 15 100       32 if ($opts->{env}) {
137 1         2 $set_env{$_} = $opts->{env}{$_} for keys %{ $opts->{env} };
  1         5  
138             }
139 15 100       33 if (%set_env) {
140 1         9 for (keys %set_env) {
141 1         6 $save_env{$_} = $ENV{$_};
142 1         21 $ENV{$_} = $set_env{$_};
143             }
144             }
145              
146 15         57 my $wa;
147             my $res;
148              
149 15         0 my $capture_stdout_was_false;
150 15         0 my $emulate_backtick;
151 15         0 my $tmp_capture_stdout;
152              
153             my $code_capture = sub {
154 13     13   19 my $doit = shift;
155              
156 13 50 66     156 if ($opts->{capture_stdout} && $opts->{capture_stderr}) {
    100 66        
    100          
    100          
    50          
    100          
    100          
    100          
157 0         0 require Capture::Tiny;
158 0         0 (${ $opts->{capture_stdout} }, ${ $opts->{capture_stderr} }) =
  0         0  
  0         0  
159             &Capture::Tiny::capture($doit);
160             } elsif ($opts->{capture_merged}) {
161 1         18 require Capture::Tiny;
162 1         74 ${ $opts->{capture_merged} } =
  1         1194  
163             &Capture::Tiny::capture_merged($doit);
164             } elsif ($opts->{capture_stdout}) {
165 3         852 require Capture::Tiny;
166 3         7134 ${ $opts->{capture_stdout} } =
  3         2761  
167             &Capture::Tiny::capture_stdout($doit);
168             } elsif ($opts->{capture_stderr}) {
169 1         18 require Capture::Tiny;
170 1         43 ${ $opts->{capture_stderr} } =
  1         851  
171             &Capture::Tiny::capture_stderr($doit);
172              
173             } elsif ($opts->{tee_stdout} && $opts->{tee_stderr}) {
174 0         0 require Capture::Tiny;
175 0         0 (${ $opts->{tee_stdout} }, ${ $opts->{tee_stderr} }) =
  0         0  
  0         0  
176             &Capture::Tiny::tee($doit);
177             } elsif ($opts->{tee_merged}) {
178 1         16 require Capture::Tiny;
179 1         69 ${ $opts->{tee_merged} } =
  1         2469  
180             &Capture::Tiny::tee_merged($doit);
181             } elsif ($opts->{tee_stdout}) {
182 1         16 require Capture::Tiny;
183 1         40 ${ $opts->{tee_stdout} } =
  1         2439  
184             &Capture::Tiny::tee_stdout($doit);
185             } elsif ($opts->{tee_stderr}) {
186 1         27 require Capture::Tiny;
187 1         78 ${ $opts->{tee_stderr} } =
  1         2394  
188             &Capture::Tiny::tee_stderr($doit);
189             } else {
190 5         13 $doit->();
191             }
192 15         53 };
193              
194 15 100 66     58 if ($which eq 'system') {
    100          
    50          
195              
196 10 50 33     120 if ($opts->{log} || $opts->{dry_run}) {
197 0 0       0 if ($opts->{log}) {
198 1     1   8 no strict 'refs';
  1         2  
  1         299  
199 0         0 my $routine;
200 0         0 my $label = "";
201 0 0       0 if ($opts->{dry_run}) {
202 0         0 $label = "[DRY RUN] ";
203 0         0 $routine = "log_info";
204             } else {
205 0         0 $routine = "log_trace";
206             }
207 0         0 $routine->("%ssystem(%s), env=%s", $label, \@args, \%set_env);
208             } else {
209 0         0 warn "[DRY RUN] system("._args2cmd(@args).")\n";
210             }
211 0 0       0 if ($opts->{dry_run}) {
212 0         0 $child_error = 0;
213 0         0 $exit_code_is_success = 1;
214 0         0 $res = "";
215 0         0 goto CHECK_RESULT;
216             }
217             }
218              
219             my $doit = sub {
220 10 50   10   42653 if ($opts->{shell}) {
    50          
221             # force the use of shell
222 0         0 $res = system _args2cmd(@args);
223             } elsif (defined $opts->{shell}) {
224             # forbid shell
225 0         0 $res = system {$args[0]} @args;
  0         0  
226             } else {
227             # might or might not use shell (if @args == 1)
228 10         45037 $res = system @args;
229             }
230 10         319 $child_error = $?;
231 10 100       449 $exit_code_is_success = $code_exit_code_is_success->($? < 0 ? $? : $? >> 8);
232 10         566 $os_error = $!;
233 10         58 };
234 10         27 $code_capture->($doit);
235              
236             } elsif ($which eq 'readpipe') {
237              
238 3         7 $wa = wantarray;
239              
240 3 50 33     36 if ($opts->{log} || $opts->{dry_run}) {
241 0 0       0 if ($opts->{log}) {
242 1     1   8 no strict 'refs';
  1         9  
  1         384  
243 0         0 my $routine;
244 0         0 my $label = "";
245 0 0       0 if ($opts->{dry_run}) {
246 0         0 $label = "[DRY RUN] ";
247 0         0 $routine = "log_info";
248             } else {
249 0         0 $routine = "log_trace";
250             }
251 0         0 $routine->("%sreadpipe(%s), env=%s", $label, _args2cmd(@args), \%set_env);
252             } else {
253 0         0 warn "[DRY RUN] readpipe("._args2cmd(@args).")\n";
254             }
255 0 0       0 if ($opts->{dry_run}) {
256 0         0 $child_error = 0;
257 0         0 $exit_code_is_success = 1;
258 0         0 $res = "";
259 0         0 goto CHECK_RESULT;
260             }
261             }
262              
263             # we want to avoid the shell, so we don't use the builtin backtick.
264             # instead, we emulate backtick by system + capturing the output
265 3 100 100     38 if (defined $opts->{shell} && !$opts->{shell}) {
266 1         11 $emulate_backtick++;
267             die "Currently cannot backtick() with options shell=0 and capture_merged|tee_*"
268 1 50 33     33 if $opts->{capture_merged} || $opts->{tee_stdout} || $opts->{tee_stderr} || $opts->{tee_merged};
      33        
      33        
269 1 50       12 if (!$opts->{capture_stdout}) {
270 1         7 $capture_stdout_was_false++;
271 1         11 $opts->{capture_stdout} = \$tmp_capture_stdout;
272             }
273             }
274              
275             my $doit = sub {
276 3 100   3   1177 if ($emulate_backtick) {
277             # we don't want shell so we have to emulate backtick with system
278             # + capture the output ourselves
279 1         7 system {$args[0]} @args;
  1         5361  
280             } else {
281 2         14 my $cmd = _args2cmd(@args);
282             #warn "cmd for backtick: " . $cmd;
283             # use backtick, which uses the shell
284 2 50       76 if ($wa) {
285 0         0 $res = [`$cmd`];
286             } else {
287 2         13073 $res = `$cmd`;
288             }
289             }
290 3 50       117 $child_error = $? < 0 ? $? : $? >> 8;
291 3 50       127 $exit_code_is_success = $code_exit_code_is_success->($? < 0 ? $? : $? >> 8);
292 3         99 $os_error = $!;
293 3         30 };
294 3         15 $code_capture->($doit);
295              
296 3 100       85 if ($emulate_backtick) {
297             $res = $capture_stdout_was_false ? $tmp_capture_stdout :
298 1 50       16 ${ $opts->{capture_stdout} };
  0         0  
299 1 50       16 $res = [split /^/m, $res] if $wa;
300 1 50       36 $opts->{capture_stdout} = undef if $capture_stdout_was_false;
301             }
302              
303             } elsif ($which eq 'run' || $which eq 'start') {
304              
305 2 50 33     28 if ($opts->{log} || $opts->{dry_run}) {
306 0 0       0 if ($opts->{log}) {
307 1     1   8 no strict 'refs';
  1         2  
  1         1148  
308 0         0 my $routine;
309 0         0 my $label = "";
310 0 0       0 if ($opts->{dry_run}) {
311 0         0 $label = "[DRY RUN] ";
312 0         0 $routine = "log_info";
313             } else {
314 0         0 $routine = "log_trace";
315             }
316 0         0 $routine->("%srun(%s), env=%s", $label,
317             join(", ", @args), \%set_env);
318             } else {
319 0         0 warn "[DRY RUN] $which(".join(", ", @args).")\n";
320             }
321 0 0       0 if ($opts->{dry_run}) {
322 0         0 $child_error = 0;
323 0         0 $exit_code_is_success = 1;
324 0         0 $res = "";
325 0         0 goto CHECK_RESULT;
326             }
327             }
328              
329 2         1233 require IPC::Run;
330 2 100       35815 my $func = $which eq 'run' ? "IPC::Run::run" : "IPC::Run::start";
331 2         19 $res = &{$func}(
332             \@args,
333             defined($opts->{stdin}) ? \$opts->{stdin} : \*STDIN,
334             sub {
335 2 50   2   10612 if ($opts->{capture_stdout}) {
336 2 50       19 if (ref $opts->{capture_stdout} eq 'CODE') {
337 0         0 $opts->{capture_stdout}->($_[0]);
338             } else {
339 2         8 ${$opts->{capture_stdout}} .= $_[0];
  2         20  
340             }
341             } else {
342 0         0 print $_[0];
343             }
344             }, # out
345             sub {
346 0 0   0   0 if ($opts->{capture_stderr}) {
347 0 0       0 if (ref $opts->{capture_sderr} eq 'CODE') {
348 0         0 $opts->{capture_sderr}->($_[0]);
349             } else {
350 0         0 ${$opts->{capture_stderr}} .= $_[0];
  0         0  
351             }
352             } else {
353 0         0 print STDERR $_[0];
354             }
355             }, # err
356 2 50       43 );
357 2 100       10182 if ($which eq 'run') {
358 1         91 $child_error = $?;
359 1 50       25 $exit_code_is_success = $code_exit_code_is_success->($? < 0 ? $? : $? >> 8);
360 1         20 $os_error = $!;
361             } else {
362 1         26 $child_error = 0;
363 1         13 $exit_code_is_success = 1;
364 1         17 $os_error = "";
365             }
366              
367             } # which
368              
369             # log output
370 15 50       116 if ($opts->{log}) {
371 0         0 my $res_show;
372 0 0       0 if (defined $opts->{max_log_output}) {
373 0         0 $res_show = '';
374 0 0       0 if ($wa) {
375 0         0 for (@$res) {
376 0 0       0 if (length($res_show) + length($_) >=
377             $opts->{max_log_output}) {
378             $res_show .= substr(
379 0         0 $_,0,$opts->{max_log_output}-length($res_show));
380 0         0 last;
381             } else {
382 0         0 $res_show .= $_;
383             }
384             }
385             } else {
386 0 0       0 if (length($res) > $opts->{max_log_output}) {
387 0         0 $res_show = substr($res, 0, $opts->{max_log_output});
388             }
389             }
390             }
391             log_trace("result of $which(): %s (%d bytes)",
392             defined($res_show) ? $res_show : $res,
393             defined($res_show) ?
394 0 0       0 $opts->{max_log_output} : length($res))
    0          
    0          
395             if $exit_code_is_success;
396             } # end of log output
397              
398             # restore ENV
399 15 100       58 if (%save_env) {
400 1         18 for (keys %save_env) {
401 1 50       13 if (defined $save_env{$_}) {
402 0         0 $ENV{$_} = $save_env{$_};
403             } else {
404 1         20 undef $ENV{$_};
405             }
406             }
407             }
408              
409             # restore previous working directory
410 15 100       39 if ($cwd) {
411 1 50       41 unless (chdir $cwd) {
412 0 0       0 $log->error("Can't chdir back to '$cwd': $!") if $log;
413 0   0     0 $child_error ||= -1;
414 0         0 $os_error = $!;
415 0         0 $extra_error = "Can't chdir back";
416 0         0 goto CHECK_RESULT;
417             }
418             }
419              
420             CHECK_RESULT:
421 16 100       58 unless ($exit_code_is_success) {
422 3 100 66     61 if ($opts->{log} || $opt_die) {
423             my $msg = sprintf(
424             "%s(%s) failed: %s (%s)%s%s%s",
425             $which,
426             join(" ", @args),
427             defined $extra_error ? "" : $child_error,
428             defined $extra_error ? "$extra_error: $os_error" : explain_child_error($child_error, $os_error),
429             (ref($opts->{capture_stdout}) ?
430             ", captured stdout: <<" .
431 0         0 (defined ${$opts->{capture_stdout}} ? ${$opts->{capture_stdout}} : ''). ">>" : ""),
  0         0  
432             (ref($opts->{capture_stderr}) ?
433             ", captured stderr: <<" .
434 0         0 (defined ${$opts->{capture_stderr}} ? ${$opts->{capture_stderr}} : ''). ">>" : ""),
  0         0  
435             (ref($opts->{capture_merged}) ?
436             ", captured merged: <<" .
437 2 100       86 (defined ${$opts->{capture_merged}} ? ${$opts->{capture_merged}} : ''). ">>" : ""),
  0 100       0  
  0 0       0  
    50          
    0          
    50          
    0          
    50          
438             );
439 2 50       112 logger($opts->{fail_log_level}, $msg) if $opts->{log};
440 2 50       99 die $msg if $opt_die;
441             }
442             }
443              
444 14 100       89 if ($which ne 'start') {
445 13         40 $? = $child_error;
446             }
447              
448 14 50 33     1053 return $wa && $which ne 'run' && $which ne 'start' ? @$res : $res;
449             }
450              
451             sub system {
452 12     12 1 47157 _system_or_readpipe_or_run_or_start('system', @_);
453             }
454              
455             # backtick is the older, deprecated name for readpipe
456             sub backtick {
457 0     0 0 0 _system_or_readpipe_or_run_or_start('readpipe', @_);
458             }
459              
460             sub readpipe {
461 3     3 1 7018 _system_or_readpipe_or_run_or_start('readpipe', @_);
462             }
463              
464             sub run {
465 1     1 1 3509 _system_or_readpipe_or_run_or_start('run', @_);
466             }
467              
468             sub start {
469 1     1 1 6977 _system_or_readpipe_or_run_or_start('start', @_);
470             }
471              
472             1;
473             # ABSTRACT: Perl's system(), readpipe()/qx, IPC::Run's run(), start() (with more options)
474              
475             __END__