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 = "0.9903";
4              
5 28     28   3688530 use v5.14;
  28         128  
6 28     28   201 use warnings;
  28         206  
  28         2324  
7 28     28   23297 use utf8;
  28         9403  
  28         199  
8 28     28   1288 use Carp;
  28         111  
  28         2477  
9 28     28   218 use Fcntl;
  28         66  
  28         9257  
10 28     28   20810 use IO::File;
  28         325286  
  28         7400  
11              
12 28     28   15391 use parent 'Command::Run::Tmpfile';
  28         10225  
  28         219  
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 165 my $code = shift;
22 69         1685 require B;
23 69         1220 my $cv = B::svref_2object($code);
24 69 50       2183 return if $cv->GV->isa('B::SPECIAL');
25 69         1148 $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 6382876 my $class = shift;
34 190         1851 my $obj = $class->SUPER::new;
35 190         1530 $obj->{OPTION} = { %default_option };
36 190         631 $obj->{RESULT} = {};
37 190 100       1163 $obj->configure(@_) if @_;
38 190         4006 $obj;
39             }
40              
41             sub configure {
42 163     163 0 295 my $obj = shift;
43 163         1039 my %args = @_;
44 163         750 for my $key (keys %args) {
45 274         553 my $val = $args{$key};
46 274 100       1383 if ($key eq 'command') {
    100          
    100          
    100          
47 134 50       980 $obj->command(ref $val eq 'ARRAY' ? @$val : $val);
48             } elsif ($key eq 'stdin') {
49 31         382 $obj->_set_stdin($val);
50             } elsif ($key eq 'stdout') {
51 17         59 $obj->{STDOUT_REF} = $val;
52             } elsif ($key eq 'stderr') {
53 33 100       180 if (ref $val eq 'SCALAR') {
54 6         18 $obj->{STDERR_REF} = $val;
55 6         39 $obj->option(stderr => 'capture');
56             } else {
57 27         207 $obj->option(stderr => $val);
58             }
59             } else {
60 59         191 $obj->option($key => $val);
61             }
62             }
63 163         621 $obj;
64             }
65              
66             sub command {
67 385     385 1 12051 my $obj = shift;
68 385 100       1082 if (@_) {
69 190         1085 $obj->{COMMAND} = [ @_ ];
70 190         4002 $obj;
71             } else {
72 195   50     308 @{$obj->{COMMAND} // []};
  195         1196  
73             }
74             }
75              
76             sub option {
77 92     92 0 168 my $obj = shift;
78 92 50       260 if (@_ == 1) {
79 0         0 return $obj->{OPTION}->{+shift};
80             } else {
81 92         516 while (my($k, $v) = splice @_, 0, 2) {
82 92         402 $obj->{OPTION}->{$k} = $v;
83             }
84 92         324 return $obj;
85             }
86             }
87              
88             sub run {
89 166     166 1 4569 my $obj = shift;
90 166         714 $obj->update(@_);
91 144 100       1382 if (my $ref = $obj->{STDOUT_REF}) {
92 13         118 $$ref = $obj->data;
93             }
94 144 100       568 if (my $ref = $obj->{STDERR_REF}) {
95 5         35 $$ref = $obj->error;
96             }
97 144         783 return $obj->result;
98             }
99              
100             sub update {
101 28     28   43697 use Time::localtime;
  28         183292  
  28         90721  
102 175     175 1 669 my $obj = shift;
103 175         553 my @command = $obj->command;
104 175 50       629 if (@command) {
105 175         892 $obj->{RESULT} = $obj->execute(\@command, @_);
106             # Store stdout in temp file for path access
107 152         2726 my $fh = $obj->fh;
108 152 50       2552 $fh->seek(0, 0) or die "seek: $!\n";
109 152 50       5072 $fh->truncate(0) or die "truncate: $!\n";
110 152   50     15875 $fh->print($obj->{RESULT}->{data} // '');
111 152         10405 $fh->flush;
112 152 50       1156 $fh->seek(0, 0) or die "seek: $!\n";
113             }
114 152         2796 $obj->date(ctime());
115 152         988 $obj;
116             }
117              
118             sub result {
119 148     148 1 389 my $obj = shift;
120 148         1455 $obj->{RESULT};
121             }
122              
123             sub execute {
124 175     175 0 284 my $obj = shift;
125 175         291 my $command = shift;
126 175         295 my %opt = (%{$obj->{OPTION}}, @_);
  175         753  
127 175 50       853 my @command = ref $command eq 'ARRAY' ? @$command : ($command);
128              
129             # Use nofork path for code references when requested
130 175 100 100     829 if ($opt{nofork} and ref $command[0] eq 'CODE') {
131 61         192 return $obj->_execute_nofork(\@command, %opt);
132             }
133              
134 114   100     858 my $stderr = $opt{stderr} // '';
135              
136             # Create pipes for stdout and stderr
137 114 50       5290 pipe(my $stdout_r, my $stdout_w) or die "pipe: $!\n";
138 114 100 50     882 pipe(my $stderr_r, my $stderr_w) or die "pipe: $!\n" if $stderr eq 'capture';
139              
140 114   50     234920 my $pid = fork // die "fork: $!\n";
141 114 100       3293 if ($pid == 0) {
142             # Child process
143 23         3202 close $stdout_r;
144 23 100       2174 close $stderr_r if $stderr eq 'capture';
145              
146 23 100       2103 if (exists $opt{stdin}) {
    100          
147 1 50       427 my $tmp = new_tmpfile IO::File or die "tmpfile: $!\n";
148 1         91 binmode $tmp, ':encoding(utf8)';
149 1         220 $tmp->print($opt{stdin});
150 1 50       65 $tmp->seek(0, 0) or die "seek: $!\n";
151 1 50       106 open STDIN, '<&', $tmp or die "dup: $!\n";
152 1         114 binmode STDIN, ':encoding(utf8)';
153             } elsif (my $input = $obj->{INPUT}) {
154 5 50       925 open STDIN, "<&=", $input->fileno or die "open: $!\n";
155 5         1013 binmode STDIN, ':encoding(utf8)';
156             }
157              
158 23 50       4298 open STDOUT, ">&=", $stdout_w->fileno or die "open stdout: $!\n";
159 23 100       3658 if ($stderr eq 'redirect') {
    100          
160 3 50       275 open STDERR, ">&STDOUT" or die "open stderr: $!\n";
161             } elsif ($stderr eq 'capture') {
162 4 50       108 open STDERR, ">&=", $stderr_w->fileno or die "open stderr: $!\n";
163             }
164             # else: stderr passes through to terminal
165              
166 23 100       1393 if (ref $command[0] eq 'CODE') {
167 8         232 my $code = shift @command;
168 8         116 @ARGV = @command;
169 8 50       450 if (my $name = code_name($code)) {
170 8         302 $0 = $name;
171             }
172 8         406 $code->(@command);
173 8         11952 exit 0;
174             }
175 15         0 exec @command;
176 0         0 die "exec: $@\n";
177             }
178              
179             # Parent process
180 91         7104 close $stdout_w;
181 91 100       1654 close $stderr_w if $stderr eq 'capture';
182              
183 91         22579 binmode $stdout_r, ':encoding(utf8)';
184 91 100       34848 binmode $stderr_r, ':encoding(utf8)' if $stderr eq 'capture';
185              
186 91         1724 my $stdout = do { local $/; <$stdout_r> };
  91         3981  
  91         29491653  
187 91 100       15440548 my $stderr_out = $stderr eq 'capture' ? do { local $/; <$stderr_r> } : '';
  7         114  
  7         350  
188              
189 91         3496 close $stdout_r;
190 91 100       819 close $stderr_r if $stderr eq 'capture';
191              
192 91         2811 waitpid $pid, 0;
193 91         1085 my $result = $?;
194              
195             return {
196 91         8049 result => $result,
197             data => $stdout,
198             error => $stderr_out,
199             pid => $pid,
200             };
201             }
202              
203             sub _tmpfile {
204 114     114   434 my ($obj, $key, %opt) = @_;
205 114 100       328 $key .= '_RAW' if $opt{raw};
206 114   66     615 my $fh = $obj->{$key} //= do {
207 104 50       23602 my $f = new_tmpfile IO::File or die "tmpfile: $!\n";
208 104 100       1493 binmode $f, $opt{raw} ? ':utf8' : ':encoding(utf8)';
209 104         3906 $f;
210             };
211 114 50       597 $fh->seek(0, 0) or die "seek: $!\n";
212 114 50       1868 $fh->truncate(0) or die "truncate: $!\n";
213 114         4959 $fh;
214             }
215              
216             sub _execute_nofork {
217 61     61   100 my $obj = shift;
218 61         71 my $command = shift;
219 61         158 my %opt = @_;
220 61         101 my @command = @$command;
221 61   100     207 my $stderr_mode = $opt{stderr} // '';
222 61         89 my $raw = $opt{raw};
223              
224 61         89 my $code = shift @command;
225              
226 61         127 my $tmp_stdout = $obj->_tmpfile('NOFORK_STDOUT', raw => $raw);
227              
228             # Save and redirect STDOUT (always needed)
229 61 50       1381 open my $save_stdout, '>&', \*STDOUT or die "dup STDOUT: $!\n";
230 61 50       16746 open STDOUT, '>&', $tmp_stdout or die "redirect STDOUT: $!\n";
231 61 100       7638 binmode STDOUT, $raw ? ':utf8' : ':encoding(utf8)';
232              
233             # Handle STDERR — only save/redirect when needed
234 61         1430 my ($save_stderr, $tmp_stderr);
235 61 100       203 if ($stderr_mode eq 'redirect') {
    100          
236 3 50       66 open $save_stderr, '>&', \*STDERR or die "dup STDERR: $!\n";
237 3 50       39 open STDERR, '>&', \*STDOUT or die "redirect STDERR: $!\n";
238             } elsif ($stderr_mode eq 'capture') {
239 9         30 $tmp_stderr = $obj->_tmpfile('NOFORK_STDERR', raw => $raw);
240 9 50       312 open $save_stderr, '>&', \*STDERR or die "dup STDERR: $!\n";
241 9 50       114 open STDERR, '>&', $tmp_stderr or die "redirect STDERR: $!\n";
242             }
243              
244             # Handle STDIN — only save/redirect when needed
245 61         1079 my $save_stdin;
246 61 100       187 if (exists $opt{stdin}) {
    100          
247 13         47 my $tmp_stdin = $obj->_tmpfile('NOFORK_STDIN', raw => $raw);
248 13         49 $tmp_stdin->print($opt{stdin});
249 13 50       135 $tmp_stdin->seek(0, 0) or die "seek: $!\n";
250 13 50       764 open $save_stdin, '<&', \*STDIN or die "dup STDIN: $!\n";
251 13 50       1724 open STDIN, '<&', $tmp_stdin or die "redirect STDIN: $!\n";
252 13 100       400 binmode STDIN, $raw ? ':utf8' : ':encoding(utf8)';
253             } elsif (my $input = $obj->{INPUT}) {
254 3 50       18 $input->seek(0, 0) or die "seek: $!\n";
255 3 50       108 open $save_stdin, '<&', \*STDIN or die "dup STDIN: $!\n";
256 3 50       36 open STDIN, '<&', $input->fileno or die "redirect STDIN: $!\n";
257 3 50       117 binmode STDIN, $raw ? ':utf8' : ':encoding(utf8)';
258             }
259              
260             # Set global state
261 61         352 local $_;
262 61         205 local @ARGV = @command;
263 61         84 my $orig_0;
264 61 50       135 if (my $name = code_name($code)) {
265 61         148 $orig_0 = $0;
266 61         420 $0 = $name;
267             }
268              
269             # Execute
270 61         171 my $result = 0;
271 61         98 eval { $code->(@command) };
  61         165  
272 61 100       4290 if ($@) {
273 6         12 $result = -1;
274             }
275              
276             # Flush and restore — only what was redirected
277 61         390 STDOUT->flush;
278 61 50       876 open STDOUT, '>&', $save_stdout or die "restore STDOUT: $!\n";
279 61 100       16949 if ($save_stderr) {
280 12         63 STDERR->flush;
281 12 50       168 open STDERR, '>&', $save_stderr or die "restore STDERR: $!\n";
282             }
283 61 100       106 if ($save_stdin) {
284 16 50       209 open STDIN, '<&', $save_stdin or die "restore STDIN: $!\n";
285             }
286 61 50       1491 if (defined $orig_0) {
287 61         327 $0 = $orig_0;
288             }
289              
290             # Read captured output from tmpfiles
291 61 50       269 $tmp_stdout->seek(0, 0) or die "seek: $!\n";
292 61         553 my $stdout_data = do { local $/; <$tmp_stdout> };
  61         203  
  61         2019  
293              
294 61         913 my $stderr_data = '';
295 61 100       145 if ($tmp_stderr) {
296 9 50       27 $tmp_stderr->seek(0, 0) or die "seek: $!\n";
297 9         69 $stderr_data = do { local $/; <$tmp_stderr> };
  9         27  
  9         216  
298             }
299              
300             return {
301 61         1344 result => $result,
302             data => $stdout_data,
303             error => $stderr_data,
304             };
305             }
306              
307             sub data {
308 25     25 1 1232 my $obj = shift;
309 25 50       249 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         325 $obj->{RESULT}->{data};
321             }
322              
323             sub error {
324 9     9 1 27 my $obj = shift;
325 9         160 $obj->{RESULT}->{error};
326             }
327              
328             sub date {
329 153     153 1 8372 my $obj = shift;
330 153 100       1380 @_ ? $obj->{DATE} = shift : $obj->{DATE};
331             }
332              
333             sub _set_stdin {
334 31     31   150 my $obj = shift;
335 31         88 my $data = shift;
336 31         209 my $input = $obj->_tmpfile('INPUT');
337 31 50       145 $input->fcntl(F_SETFD, 0) or die "fcntl F_SETFD: $!\n";
338 31         442 $input->print($data);
339 31 50       364 $input->seek(0, 0) or die "seek: $!\n";
340 31         1962 $obj;
341             }
342              
343             sub with {
344 29     29 1 91 my $obj = shift;
345 29         235 $obj->configure(@_);
346             }
347              
348             1;
349              
350             __END__