File Coverage

blib/lib/Command/Run.pm
Criterion Covered Total %
statement 216 229 94.3
branch 113 164 68.9
condition 13 18 72.2
subroutine 24 25 96.0
pod 9 14 64.2
total 375 450 83.3


line stmt bran cond sub pod time code
1             package Command::Run;
2              
3             our $VERSION = "1.00";
4              
5 28     28   2633854 use v5.14;
  28         91  
6 28     28   143 use warnings;
  28         189  
  28         1683  
7 28     28   10374 use utf8;
  28         5930  
  28         157  
8 28     28   781 use Carp;
  28         62  
  28         1712  
9 28     28   138 use Fcntl;
  28         36  
  28         5712  
10 28     28   13338 use IO::File;
  28         242118  
  28         5433  
11              
12 28     28   12076 use parent 'Command::Run::Tmpfile';
  28         7473  
  28         166  
13              
14             our $debug;
15             sub debug {
16 0     0 0 0 my $obj = shift;
17 0 0       0 @_ ? $debug = shift : $debug;
18             }
19              
20             sub code_name {
21 69     69 0 147 my $code = shift;
22 69         788 require B;
23 69         876 my $cv = B::svref_2object($code);
24 69 50       1821 return if $cv->GV->isa('B::SPECIAL');
25 69         885 $cv->GV->NAME;
26             }
27              
28             my %default_option = (
29             stderr => undef, # undef: pass-through, 'redirect': merge to stdout, 'capture': separate capture
30             );
31              
32             sub new {
33 190     190 1 4547435 my $class = shift;
34 190         1840 my $obj = $class->SUPER::new;
35 190         1082 $obj->{OPTION} = { %default_option };
36 190         473 $obj->{RESULT} = {};
37 190 100       1109 $obj->configure(@_) if @_;
38 190         2288 $obj;
39             }
40              
41             sub configure {
42 163     163 0 228 my $obj = shift;
43 163         795 my %args = @_;
44 163         663 for my $key (keys %args) {
45 274         446 my $val = $args{$key};
46 274 100       1006 if ($key eq 'command') {
    100          
    100          
    100          
47 134 50       611 $obj->command(ref $val eq 'ARRAY' ? @$val : $val);
48             } elsif ($key eq 'stdin') {
49 31         215 $obj->_set_stdin($val);
50             } elsif ($key eq 'stdout') {
51 17         60 $obj->{STDOUT_REF} = $val;
52             } elsif ($key eq 'stderr') {
53 33 100       137 if (ref $val eq 'SCALAR') {
54 6         33 $obj->{STDERR_REF} = $val;
55 6         36 $obj->option(stderr => 'capture');
56             } else {
57 27         166 $obj->option(stderr => $val);
58             }
59             } else {
60 59         94 $obj->option($key => $val);
61             }
62             }
63 163         624 $obj;
64             }
65              
66             sub command {
67 385     385 1 8962 my $obj = shift;
68 385 100       763 if (@_) {
69 190         725 $obj->{COMMAND} = [ @_ ];
70 190         2896 $obj;
71             } else {
72 195   50     220 @{$obj->{COMMAND} // []};
  195         868  
73             }
74             }
75              
76             sub option {
77 92     92 0 122 my $obj = shift;
78 92 50       200 if (@_ == 1) {
79 0         0 return $obj->{OPTION}->{+shift};
80             } else {
81 92         353 while (my($k, $v) = splice @_, 0, 2) {
82 92         305 $obj->{OPTION}->{$k} = $v;
83             }
84 92         194 return $obj;
85             }
86             }
87              
88             sub run {
89 166     166 1 3629 my $obj = shift;
90 166         622 $obj->update(@_);
91 144 100       596 if (my $ref = $obj->{STDOUT_REF}) {
92 13         152 $$ref = $obj->data;
93             }
94 144 100       503 if (my $ref = $obj->{STDERR_REF}) {
95 5         41 $$ref = $obj->error;
96             }
97 144         555 return $obj->result;
98             }
99              
100             sub update {
101 28     28   29862 use Time::localtime;
  28         119922  
  28         56464  
102 175     175 1 319 my $obj = shift;
103 175         360 my @command = $obj->command;
104 175 50       347 if (@command) {
105 175         699 $obj->{RESULT} = $obj->execute(\@command, @_);
106             # Store stdout in temp file for path access
107 152         2412 my $fh = $obj->fh;
108 152 50       1969 $fh->seek(0, 0) or die "seek: $!\n";
109 152 50       3482 $fh->truncate(0) or die "truncate: $!\n";
110 152   50     8405 $fh->print($obj->{RESULT}->{data} // '');
111 152         7596 $fh->flush;
112 152 50       786 $fh->seek(0, 0) or die "seek: $!\n";
113             }
114 152         2374 $obj->date(ctime());
115 152         826 $obj;
116             }
117              
118             sub result {
119 148     148 1 274 my $obj = shift;
120 148         1592 $obj->{RESULT};
121             }
122              
123             sub execute {
124 175     175 0 237 my $obj = shift;
125 175         238 my $command = shift;
126 175         223 my %opt = (%{$obj->{OPTION}}, @_);
  175         614  
127 175 50       589 my @command = ref $command eq 'ARRAY' ? @$command : ($command);
128              
129             # Use nofork path for code references when requested
130 175 100 100     681 if ($opt{nofork} and ref $command[0] eq 'CODE') {
131 61         162 return $obj->_execute_nofork(\@command, %opt);
132             }
133              
134 114   100     760 my $stderr = $opt{stderr} // '';
135              
136             # Create pipes for stdout and stderr
137 114 50       3818 pipe(my $stdout_r, my $stdout_w) or die "pipe: $!\n";
138 114 100 50     737 pipe(my $stderr_r, my $stderr_w) or die "pipe: $!\n" if $stderr eq 'capture';
139              
140 114   50     158376 my $pid = fork // die "fork: $!\n";
141 114 100       2875 if ($pid == 0) {
142             # Child process
143 23         3428 close $stdout_r;
144 23 100       1665 close $stderr_r if $stderr eq 'capture';
145              
146 23 100       1722 if (exists $opt{stdin}) {
    100          
147 1 50       619 my $tmp = new_tmpfile IO::File or die "tmpfile: $!\n";
148 1         121 binmode $tmp, ':encoding(utf8)';
149 1         250 $tmp->print($opt{stdin});
150 1 50       89 $tmp->seek(0, 0) or die "seek: $!\n";
151 1 50       184 open STDIN, '<&', $tmp or die "dup: $!\n";
152 1         120 binmode STDIN, ':encoding(utf8)';
153             } elsif (my $input = $obj->{INPUT}) {
154 5 50       553 open STDIN, "<&=", $input->fileno or die "open: $!\n";
155 5         868 binmode STDIN, ':encoding(utf8)';
156             }
157              
158 23 50       2290 open STDOUT, ">&=", $stdout_w->fileno or die "open stdout: $!\n";
159 23 100       3014 if ($stderr eq 'redirect') {
    100          
160 3 50       156 open STDERR, ">&STDOUT" or die "open stderr: $!\n";
161             } elsif ($stderr eq 'capture') {
162 4 50       95 open STDERR, ">&=", $stderr_w->fileno or die "open stderr: $!\n";
163             }
164             # else: stderr passes through to terminal
165              
166 23 100       1026 if (ref $command[0] eq 'CODE') {
167 8         240 my $code = shift @command;
168 8         166 @ARGV = @command;
169 8 50       377 if (my $name = code_name($code)) {
170 8         319 $0 = $name;
171             }
172 8         269 $code->(@command);
173 8         7365 exit 0;
174             }
175 15         0 exec @command;
176 0         0 die "exec: $@\n";
177             }
178              
179             # Parent process
180 91         6329 close $stdout_w;
181 91 100       1333 close $stderr_w if $stderr eq 'capture';
182              
183 91         18747 binmode $stdout_r, ':encoding(utf8)';
184 91 100       53531 binmode $stderr_r, ':encoding(utf8)' if $stderr eq 'capture';
185              
186 91         928 my $stdout = do { local $/; <$stdout_r> };
  91         3388  
  91         18219291  
187 91 100       10643059 my $stderr_out = $stderr eq 'capture' ? do { local $/; <$stderr_r> } : '';
  7         188  
  7         421  
188              
189 91         3498 close $stdout_r;
190 91 100       711 close $stderr_r if $stderr eq 'capture';
191              
192 91         2453 waitpid $pid, 0;
193 91         936 my $result = $?;
194              
195             return {
196 91         6263 result => $result,
197             data => $stdout,
198             error => $stderr_out,
199             pid => $pid,
200             };
201             }
202              
203             sub _tmpfile {
204 114     114   315 my ($obj, $key, %opt) = @_;
205 114 100       269 $key .= '_RAW' if $opt{raw};
206 114   66     497 my $fh = $obj->{$key} //= do {
207 104 50       23270 my $f = new_tmpfile IO::File or die "tmpfile: $!\n";
208 104 100       1304 binmode $f, $opt{raw} ? ':utf8' : ':encoding(utf8)';
209 104         3294 $f;
210             };
211 114 50       513 $fh->seek(0, 0) or die "seek: $!\n";
212 114 50       1182 $fh->truncate(0) or die "truncate: $!\n";
213 114         3772 $fh;
214             }
215              
216             sub _execute_nofork {
217 61     61   60 my $obj = shift;
218 61         53 my $command = shift;
219 61         112 my %opt = @_;
220 61         73 my @command = @$command;
221 61   100     165 my $stderr_mode = $opt{stderr} // '';
222 61         72 my $raw = $opt{raw};
223              
224 61         75 my $code = shift @command;
225              
226 61         110 my $tmp_stdout = $obj->_tmpfile('NOFORK_STDOUT', raw => $raw);
227              
228             # Save and redirect STDOUT (always needed)
229 61 50       949 open my $save_stdout, '>&', \*STDOUT or die "dup STDOUT: $!\n";
230 61 50       12233 open STDOUT, '>&', $tmp_stdout or die "redirect STDOUT: $!\n";
231 61 100       2200 binmode STDOUT, $raw ? ':utf8' : ':encoding(utf8)';
232              
233             # Handle STDERR — only save/redirect when needed
234 61         1020 my ($save_stderr, $tmp_stderr);
235 61 100       146 if ($stderr_mode eq 'redirect') {
    100          
236 3 50       42 open $save_stderr, '>&', \*STDERR or die "dup STDERR: $!\n";
237 3 50       21 open STDERR, '>&', \*STDOUT or die "redirect STDERR: $!\n";
238             } elsif ($stderr_mode eq 'capture') {
239 9         27 $tmp_stderr = $obj->_tmpfile('NOFORK_STDERR', raw => $raw);
240 9 50       126 open $save_stderr, '>&', \*STDERR or die "dup STDERR: $!\n";
241 9 50       66 open STDERR, '>&', $tmp_stderr or die "redirect STDERR: $!\n";
242             }
243              
244             # Handle STDIN — only save/redirect when needed
245 61         650 my $save_stdin;
246 61 100       175 if (exists $opt{stdin}) {
    100          
247 13         39 my $tmp_stdin = $obj->_tmpfile('NOFORK_STDIN', raw => $raw);
248 13         48 $tmp_stdin->print($opt{stdin});
249 13 50       105 $tmp_stdin->seek(0, 0) or die "seek: $!\n";
250 13 50       642 open $save_stdin, '<&', \*STDIN or die "dup STDIN: $!\n";
251 13 50       1099 open STDIN, '<&', $tmp_stdin or die "redirect STDIN: $!\n";
252 13 100       443 binmode STDIN, $raw ? ':utf8' : ':encoding(utf8)';
253             } elsif (my $input = $obj->{INPUT}) {
254 3 50       9 $input->seek(0, 0) or die "seek: $!\n";
255 3 50       60 open $save_stdin, '<&', \*STDIN or die "dup STDIN: $!\n";
256 3 50       12 open STDIN, '<&', $input->fileno or die "redirect STDIN: $!\n";
257 3 50       63 binmode STDIN, $raw ? ':utf8' : ':encoding(utf8)';
258             }
259              
260             # Set global state
261 61         262 local $_;
262 61         123 local @ARGV = @command;
263 61         63 my $orig_0;
264 61 50       175 if (my $name = code_name($code)) {
265 61         120 $orig_0 = $0;
266 61         371 $0 = $name;
267             }
268              
269             # Execute
270 61         87 my $result = 0;
271 61         64 eval { $code->(@command) };
  61         140  
272 61 100       3361 if ($@) {
273 6         9 $result = -1;
274             }
275              
276             # Flush and restore — only what was redirected
277 61         283 STDOUT->flush;
278 61 50       838 open STDOUT, '>&', $save_stdout or die "restore STDOUT: $!\n";
279 61 100       12052 if ($save_stderr) {
280 12         33 STDERR->flush;
281 12 50       147 open STDERR, '>&', $save_stderr or die "restore STDERR: $!\n";
282             }
283 61 100       89 if ($save_stdin) {
284 16 50       141 open STDIN, '<&', $save_stdin or die "restore STDIN: $!\n";
285             }
286 61 50       962 if (defined $orig_0) {
287 61         252 $0 = $orig_0;
288             }
289              
290             # Read captured output from tmpfiles
291 61 50       194 $tmp_stdout->seek(0, 0) or die "seek: $!\n";
292 61         436 my $stdout_data = do { local $/; <$tmp_stdout> };
  61         171  
  61         1441  
293              
294 61         547 my $stderr_data = '';
295 61 100       92 if ($tmp_stderr) {
296 9 50       15 $tmp_stderr->seek(0, 0) or die "seek: $!\n";
297 9         57 $stderr_data = do { local $/; <$tmp_stderr> };
  9         18  
  9         165  
298             }
299              
300             return {
301 61         1021 result => $result,
302             data => $stdout_data,
303             error => $stderr_data,
304             };
305             }
306              
307             sub data {
308 25     25 1 848 my $obj = shift;
309 25 50       152 if (@_) {
310 0         0 my $data = shift;
311 0         0 $obj->{RESULT}->{data} = $data;
312 0         0 my $fh = $obj->fh;
313 0 0       0 $fh->seek(0, 0) or die "seek: $!\n";
314 0 0       0 $fh->truncate(0) or die "truncate: $!\n";
315 0         0 $fh->print($data);
316 0         0 $fh->flush;
317 0 0       0 $fh->seek(0, 0) or die "seek: $!\n";
318 0         0 return $obj;
319             }
320 25         388 $obj->{RESULT}->{data};
321             }
322              
323             sub error {
324 9     9 1 27 my $obj = shift;
325 9         40 $obj->{RESULT}->{error};
326             }
327              
328             sub date {
329 153     153 1 6149 my $obj = shift;
330 153 100       1203 @_ ? $obj->{DATE} = shift : $obj->{DATE};
331             }
332              
333             sub _set_stdin {
334 31     31   101 my $obj = shift;
335 31         70 my $data = shift;
336 31         145 my $input = $obj->_tmpfile('INPUT');
337 31 50       200 $input->fcntl(F_SETFD, 0) or die "fcntl F_SETFD: $!\n";
338 31         409 $input->print($data);
339 31 50       298 $input->seek(0, 0) or die "seek: $!\n";
340 31         1556 $obj;
341             }
342              
343             sub with {
344 29     29 1 75 my $obj = shift;
345 29         130 $obj->configure(@_);
346             }
347              
348             1;
349              
350             __END__