File Coverage

lib/BATsh/CMD.pm
Criterion Covered Total %
statement 655 1008 64.9
branch 280 528 53.0
condition 61 161 37.8
subroutine 43 54 79.6
pod 0 4 0.0
total 1039 1755 59.2


line stmt bran cond sub pod time code
1             package BATsh::CMD;
2             # Copyright (c) 2026 INABA Hitoshi
3             ######################################################################
4             #
5             # BATsh::CMD - Pure Perl cmd.exe interpreter
6             #
7             # v0.02 changes (cmd.exe compatibility fixes):
8             # 1. Environment variable case-insensitivity (via Env.pm)
9             # 2. ^ escape character: protects & | < > and line continuation
10             # 3. Redirect/pipe: > >> 2> 2>> < | parsed before dispatch
11             # 4. SETLOCAL ENABLEDELAYEDEXPANSION + !VAR! (via Env.pm)
12             # 5. IF block pre-expansion: entire IF block expanded at parse time
13             # (matching cmd.exe's "parse before execute" semantics)
14             # 6. FOR /F: tokens= delims= skip= eol= usebackq
15             # 7. IF /I must be parsed BEFORE plain == to avoid shadowing
16             # 8. ECHO no longer resets ERRORLEVEL
17             # 9. SETLOCAL passes option string to Env::setlocal()
18             # 10. IF EXIST handles quoted paths with spaces
19             # 11. Pipeline (|): _split_compound detects |, _exec_pipe chains via tmpfile
20             # 12. SET /P VAR=Prompt: reads one line from STDIN
21             # 13. SHIFT / SHIFT /N: shifts %1..%9 and %* positional parameters
22             # 14. Batch-parameter tilde modifiers via Env::expand_cmd():
23             # %~0 %~f1 %~d0 %~p0 %~n1 %~x1 %~dp0 %~nx1 (f d p n x combinable)
24             # 15. & && || compound commands (_exec_compound)
25             # 16. %0..%9 and %* positional parameter expansion in expand_cmd()
26             #
27             ######################################################################
28              
29 7     7   45 use strict;
  7         14  
  7         583  
30 7 50 33 7   175 BEGIN { if ($] < 5.006 && !defined(&warnings::import)) { $INC{'warnings.pm'} = 'stub'; eval 'package warnings; sub import {}' } }
  0         0  
  0         0  
31 7     7   33 use warnings; local $^W = 1;
  7         13  
  7         525  
32 7 50   7   184 BEGIN { pop @INC if $INC[-1] eq '.' }
33              
34 7     7   36 use File::Spec ();
  7         11  
  7         114  
35 7     7   3884 use File::Copy ();
  7         36231  
  7         225  
36 7     7   48 use File::Path ();
  7         11  
  7         138  
37 7     7   27 use Carp qw(croak);
  7         9  
  7         383  
38 7     7   34 use vars qw($VERSION);
  7         9  
  7         18983  
39             $VERSION = '0.02';
40             $VERSION = $VERSION;
41              
42             require BATsh::Env;
43              
44             # ----------------------------------------------------------------
45             # Module-level state
46             # ----------------------------------------------------------------
47             my $ECHO_ON = 1;
48             my $ERRORLEVEL = 0;
49             my $_GOTO_LABEL = '';
50              
51             # ----------------------------------------------------------------
52             # Public: execute an array of CMD lines
53             # ----------------------------------------------------------------
54             sub exec_block {
55 61     61 0 6176 my ($class, $lines_ref, %opts) = @_;
56 61         87 my @lines = @{$lines_ref};
  61         138  
57              
58             # Preprocess: join ^ line-continuations
59 61         143 @lines = _join_continuations(@lines);
60              
61             # Build label index
62 61         95 my %labels = ();
63 61         170 for my $i (0 .. $#lines) {
64 133         182 my $l = $lines[$i];
65 133         153 $l =~ s/\r?\n\z//;
66 133         195 $l =~ s/\A\s+//;
67 133 100       251 if ($l =~ /\A:([A-Za-z_][A-Za-z0-9_]*)\s*\z/) {
68 1         5 $labels{uc($1)} = $i;
69             }
70             }
71              
72 61         90 my $i = 0;
73 61         112 while ($i <= $#lines) {
74 110         137 my $raw = $lines[$i];
75 110         115 $i++;
76 110         133 $raw =~ s/\r?\n\z//;
77 110         213 my $rc = _exec_line($class, $raw, \@lines, \%labels, \$i, \%opts);
78              
79 110 100       246 if ($_GOTO_LABEL ne '') {
80 1         2 my $lbl = $_GOTO_LABEL;
81 1         1 $_GOTO_LABEL = '';
82 1 50       3 if (exists $labels{$lbl}) {
    0          
83 1         2 $i = $labels{$lbl} + 1;
84             }
85             elsif ($lbl eq 'EOF') {
86 0         0 last;
87             }
88             else {
89 0         0 _warn("GOTO: label :$lbl not found");
90             }
91 1         2 next;
92             }
93 109 50 33     410 if (defined $rc && $rc eq '__EXIT__') {
94 0         0 return $ERRORLEVEL;
95             }
96             }
97 61         308 return $ERRORLEVEL;
98             }
99              
100             # ----------------------------------------------------------------
101             # _join_continuations: merge lines ending with bare ^ (not ^^ or "^")
102             # cmd.exe rule: ^ at end-of-line (outside quotes) joins next line,
103             # consuming the ^ and leading whitespace of the next line.
104             # ----------------------------------------------------------------
105             sub _join_continuations {
106 61     61   128 my @in = @_;
107 61         58 my @out;
108 61         77 my $i = 0;
109 61         144 while ($i <= $#in) {
110 133         171 my $line = $in[$i]; $i++;
  133         117  
111 133         404 $line =~ s/\r?\n\z//;
112             # Count unescaped ^ at end: odd count = continuation
113 133         291 while ($line =~ /\A((?:[^^]|\^\^)*)\^\z/) {
114             # Strip the trailing ^, append next line (minus leading ws)
115 1         4 $line = $1;
116 1 50       3 if ($i <= $#in) {
117 1         2 my $next = $in[$i]; $i++;
  1         1  
118 1         4 $next =~ s/\r?\n\z//;
119 1         2 $next =~ s/\A\s+//;
120 1         3 $line .= $next;
121             }
122 0         0 else { last }
123             }
124 133         237 push @out, $line;
125             }
126 61         165 return @out;
127             }
128              
129             # ----------------------------------------------------------------
130             # _unescape_caret: replace ^X with X (^ is escape char in cmd.exe)
131             # Called AFTER %VAR% expansion for non-block contexts.
132             # ----------------------------------------------------------------
133             sub _unescape_caret {
134 47     47   58 my ($str) = @_;
135 47         64 $str =~ s/\^(.)/$1/g;
136 47         63 return $str;
137             }
138              
139             # ----------------------------------------------------------------
140             # Execute one logical line
141             # $pre_expanded: if true, skip %VAR% expansion (already done by FOR)
142             # $block_expanded: if true, skip expansion entirely (IF block body)
143             # ----------------------------------------------------------------
144             sub _exec_line {
145 157     157   306 my ($class, $raw, $lines_ref, $labels_ref, $i_ref, $opts_ref, $pre_expanded) = @_;
146 157 100       254 $pre_expanded = 0 unless defined $pre_expanded;
147              
148 157         194 my $line = $raw;
149 157         279 $line =~ s/\A\s+//;
150              
151 157         165 my $suppress_echo = 0;
152 157 100       251 if ($line =~ s/\A\@//) { $suppress_echo = 1; }
  1         2  
153              
154 157 50       335 return 0 if $line =~ /\A\s*\z/;
155 157 50       242 return 0 if $line =~ /\A::/;
156 157 50       344 return 0 if $line =~ /\AREM(?:\s|\z)/i;
157 157 50       282 return 0 if $line =~ /\A:[A-Za-z_]/;
158 157 50       247 return 0 if $line =~ /\A\s*\)\s*(?:ELSE\s*.*)??\s*\z/i;
159 157 50       1158 return 0 if $line =~ /\A#/;
160              
161 157 100       301 if (!$pre_expanded) {
    100          
162 110         166 $line = _expand_line($line);
163             }
164             elsif (BATsh::Env::delayed_expansion()) {
165             # Even in pre_expanded blocks, !VAR! must be expanded at runtime
166 8         11 $line = BATsh::Env->expand_cmd($line);
167             }
168              
169             # Handle compound commands: & && || (outside quotes, after expansion)
170             # Split on & / && / || and execute left to right
171 157 100       324 if ($line =~ /[&|]/) {
172 4         18 my @parts = _split_compound($line);
173 4 100       12 if (@parts > 1) {
174 3         14 return _exec_compound($class, \@parts, $lines_ref, $labels_ref, $i_ref, $opts_ref);
175             }
176             }
177              
178             # Handle redirection stripping before dispatch
179 154         249 my ($clean_line, $redirs) = _strip_redirects($line);
180              
181 154         333 return _dispatch_with_redirs($class, $clean_line, $redirs, $lines_ref, $labels_ref, $i_ref, $opts_ref);
182             }
183              
184             # ----------------------------------------------------------------
185             # _expand_line: %VAR% expansion protecting FOR loop variables
186             # ----------------------------------------------------------------
187             sub _expand_line {
188 110     110   154 my ($line) = @_;
189 110 100       203 if ($line =~ /\AFOR\s/i) {
190 11 50       77 if ($line =~ /\A(FOR\s+(?:\/[A-Za-z]\s+(?:"[^"]*"\s+)?)?%%[A-Za-z]\s+(?:\/[A-Za-z]\s+)?IN\s*\([^)]*\)\s+DO\s+)(.*)\z/i) {
191 11         38 my ($for_head, $do_part) = ($1, $2);
192 11         45 $for_head =~ s/%%([A-Za-z])/"\x00FOR_$1\x00"/ge;
  11         47  
193 11         48 $for_head = BATsh::Env->expand_cmd($for_head);
194 11         66 $for_head =~ s/\x00FOR_([A-Za-z])\x00/%%$1/g;
195 11         27 $do_part =~ s/%%([A-Za-z])/"\x00FOR_$1\x00"/ge;
  10         24  
196 11         25 $do_part =~ s/%([^%\r\n]+)%/"\x00PCT_$1\x00"/ge;
  3         8  
197 11         35 return $for_head . $do_part;
198             }
199             else {
200 0         0 $line =~ s/%%([A-Za-z])/"\x00FOR_$1\x00"/ge;
  0         0  
201 0         0 $line = BATsh::Env->expand_cmd($line);
202 0         0 $line =~ s/\x00FOR_([A-Za-z])\x00/%%$1/g;
203 0         0 return $line;
204             }
205             }
206             else {
207 99         135 $line =~ s/%%([A-Za-z])/"\x00FOR_$1\x00"/ge;
  0         0  
208 99         319 $line = BATsh::Env->expand_cmd($line);
209 99         151 $line =~ s/\x00FOR_([A-Za-z])\x00/%%$1/g;
210 99         167 return $line;
211             }
212             }
213              
214             # ----------------------------------------------------------------
215             # _split_compound: split on bare & && || (respecting quotes and ^)
216             # Returns list of { op => '&'|'&&'|'||'|'', cmd => $str }
217             # ----------------------------------------------------------------
218             sub _split_compound {
219 6     6   756 my ($line) = @_;
220 6         12 my @parts;
221 6         12 my $cur = '';
222 6         10 my $in_q = 0;
223 6         62 my @chars = split //, $line;
224 6         13 my $n = scalar @chars;
225 6         7 my $j = 0;
226 6         19 while ($j < $n) {
227 126         147 my $ch = $chars[$j];
228 126 100 66     189 if ($ch eq '^' && !$in_q) {
229             # escaped: take next char literally
230 1         1 $j++;
231 1 50       4 if ($j < $n) { $cur .= $chars[$j]; $j++ }
  1         2  
  1         2  
232 1         2 next;
233             }
234 125 50       192 if ($ch eq '"') { $in_q = !$in_q; $cur .= $ch; $j++; next }
  0         0  
  0         0  
  0         0  
  0         0  
235 125 100 66     260 if (!$in_q && $ch eq '&') {
236 2 100 66     12 if ($j+1 < $n && $chars[$j+1] eq '&') {
237 1         6 push @parts, { op => '', cmd => $cur }; $cur = ''; $j += 2;
  1         2  
  1         3  
238 1         3 push @parts, { op => '&&', cmd => '' };
239             }
240             else {
241 1         6 push @parts, { op => '', cmd => $cur }; $cur = ''; $j++;
  1         3  
  1         2  
242 1         3 push @parts, { op => '&', cmd => '' };
243             }
244 2         5 next;
245             }
246 123 100 66     265 if (!$in_q && $ch eq '|') {
247 4 50 33     43 if ($j+1 < $n && $chars[$j+1] eq '|') {
    50 33        
248 0         0 push @parts, { op => '', cmd => $cur }; $cur = ''; $j += 2;
  0         0  
  0         0  
249 0         0 push @parts, { op => '||', cmd => '' };
250             }
251             elsif ($j+1 < $n && $chars[$j+1] ne '>') {
252             # pipe: record left side and mark as pipe op
253 4         41 push @parts, { op => '', cmd => $cur }; $cur = ''; $j++;
  4         5  
  4         7  
254 4         26 push @parts, { op => '|', cmd => '' };
255             }
256             else {
257 0         0 $cur .= $ch; $j++; next;
  0         0  
  0         0  
258             }
259 4         37 next;
260             }
261 119         112 $cur .= $ch; $j++;
  119         140  
262             }
263 6 50       49 push @parts, { op => '', cmd => $cur } if $cur =~ /\S/;
264 6         33 return @parts;
265             }
266              
267             # ----------------------------------------------------------------
268             # _exec_compound: execute & / && / || compound commands
269             # ----------------------------------------------------------------
270             sub _exec_compound {
271 3     3   11 my ($class, $parts, $lines_ref, $labels_ref, $i_ref, $opts_ref) = @_;
272              
273             # If any part uses pipe operator, delegate entirely to _exec_pipe
274             # before executing any segment (so left-side stdout is captured first).
275 3         95 for my $part (@{$parts}) {
  3         9  
276 8 100       17 if ($part->{op} eq '|') {
277 1         10 return _exec_pipe($class, $parts, $lines_ref, $labels_ref, $i_ref, $opts_ref);
278             }
279             }
280              
281 2         5 my $pending_op = '';
282 2         3 my $rc = 0;
283 2         3 for my $part (@{$parts}) {
  2         8  
284 6         12 my $op = $part->{op};
285 6         9 my $cmd = $part->{cmd};
286 6         12 $cmd =~ s/\A\s+//; $cmd =~ s/\s+\z//;
  6         19  
287              
288 6 100       15 if ($op eq '') {
289             # This is a command to run, pending_op tells us under what condition
290 4 100       27 if ($pending_op eq '') {
    100          
    50          
    50          
291 2 50       13 $rc = _exec_single($class, $cmd, $lines_ref, $labels_ref, $i_ref, $opts_ref) if $cmd =~ /\S/;
292             }
293             elsif ($pending_op eq '&&') {
294 1 50 33     9 $rc = _exec_single($class, $cmd, $lines_ref, $labels_ref, $i_ref, $opts_ref) if $ERRORLEVEL == 0 && $cmd =~ /\S/;
295             }
296             elsif ($pending_op eq '||') {
297 0 0 0     0 $rc = _exec_single($class, $cmd, $lines_ref, $labels_ref, $i_ref, $opts_ref) if $ERRORLEVEL != 0 && $cmd =~ /\S/;
298             }
299             elsif ($pending_op eq '&') {
300 1 50       8 $rc = _exec_single($class, $cmd, $lines_ref, $labels_ref, $i_ref, $opts_ref) if $cmd =~ /\S/;
301             }
302 4         9 $pending_op = '';
303             }
304             else {
305 2         5 $pending_op = $op;
306             }
307             }
308 2         11 return $rc;
309             }
310              
311             sub _exec_single {
312 6     6   17 my ($class, $cmd, $lines_ref, $labels_ref, $i_ref, $opts_ref) = @_;
313 6 50       23 return 0 unless $cmd =~ /\S/;
314 6         15 my ($clean, $redirs) = _strip_redirects($cmd);
315 6         15 return _dispatch_with_redirs($class, $clean, $redirs, $lines_ref, $labels_ref, $i_ref, $opts_ref);
316             }
317              
318             # ----------------------------------------------------------------
319             # _exec_pipe: execute cmd1 | cmd2 [| cmd3 ...] via temporary files.
320             # Left side stdout -> tmpfile; right side reads tmpfile as stdin.
321             # Perl 5.005_03 compatible: bareword filehandles, 2-arg open only.
322             # ----------------------------------------------------------------
323 7     7   68 use vars qw(*_PIPE_SAVOUT *_PIPE_SAVIN *_PIPE_WFH *_PIPE_RFH);
  7         13  
  7         8970  
324              
325             sub _exec_pipe {
326 1     1   3 my ($class, $parts, $lines_ref, $labels_ref, $i_ref, $opts_ref) = @_;
327              
328             # Collect command segments from parts list.
329             # parts layout (from _split_compound):
330             # { op=>'', cmd=>'left_cmd ' }
331             # { op=>'|', cmd=>'' }
332             # { op=>'', cmd=>' right_cmd'}
333             # Segments are op='' chunks; '|' entries are separators.
334 1         2 my @segments;
335 1         1 my $cur = '';
336 1         1 for my $part (@{$parts}) {
  1         2  
337 3         2 my $op = $part->{op};
338 3         4 my $cmd = $part->{cmd};
339 3 100       7 if ($op eq '|') {
    50          
340 1         2 push @segments, $cur;
341 1         2 $cur = '';
342             }
343             elsif ($op eq '') {
344 2         4 $cur .= $cmd;
345             }
346             else {
347             # &&, ||, & after a pipe: attach to current segment
348 0         0 $cur .= " $op $cmd";
349             }
350             }
351 1         2 push @segments, $cur;
352              
353 1         2 my $rc = 0;
354 1         51 my $base = File::Spec->catfile(File::Spec->tmpdir(),
355             "batsh_pipe_$$");
356 1         2 my $input_f = undef; # tmpfile feeding this segment's stdin
357 1         2 my $n_segs = scalar @segments;
358              
359 1         3 for my $idx (0 .. $n_segs - 1) {
360 2         3 my $seg = $segments[$idx];
361 2         8 $seg =~ s/\A\s+//; $seg =~ s/\s+\z//;
  2         8  
362 2 50       7 next unless $seg =~ /\S/;
363              
364 2 100       4 my $is_last = ($idx == $n_segs - 1) ? 1 : 0;
365 2 100       7 my $output_f = $is_last ? undef : "${base}_${idx}.tmp";
366              
367             # --- redirect STDIN from previous segment output ---
368 2         2 my $saved_in = 0;
369 2 100 66     18 if (defined $input_f && -f $input_f) {
370             open(_PIPE_RFH, $input_f)
371 1 50       20 or do { _warn("pipe: open $input_f: $!"); last };
  0         0  
  0         0  
372             open(_PIPE_SAVIN, '<&STDIN')
373 1 50       13 or do { close(_PIPE_RFH); last };
  0         0  
  0         0  
374             open(STDIN, '<&_PIPE_RFH')
375 1 50       12 or do { close(_PIPE_RFH); open(STDIN,'<&_PIPE_SAVIN'); close(_PIPE_SAVIN); last };
  0         0  
  0         0  
  0         0  
  0         0  
376 1         2 close(_PIPE_RFH);
377 1         2 $saved_in = 1;
378             }
379              
380             # --- redirect STDOUT to next segment input file ---
381 2         9 my $saved_out = 0;
382 2 100       4 if (defined $output_f) {
383             open(_PIPE_WFH, ">$output_f")
384 1 50       174 or do {
385 0 0       0 if ($saved_in) { open(STDIN,'<&_PIPE_SAVIN'); close(_PIPE_SAVIN) }
  0         0  
  0         0  
386 0         0 _warn("pipe: open $output_f: $!");
387 0         0 last;
388             };
389             open(_PIPE_SAVOUT, '>&STDOUT')
390 1 50       25 or do {
391 0         0 close(_PIPE_WFH);
392 0 0       0 if ($saved_in) { open(STDIN,'<&_PIPE_SAVIN'); close(_PIPE_SAVIN) }
  0         0  
  0         0  
393 0         0 last;
394             };
395             open(STDOUT, '>&_PIPE_WFH')
396 1 50       22 or do {
397 0         0 close(_PIPE_WFH);
398 0         0 open(STDOUT,'>&_PIPE_SAVOUT'); close(_PIPE_SAVOUT);
  0         0  
399 0 0       0 if ($saved_in) { open(STDIN,'<&_PIPE_SAVIN'); close(_PIPE_SAVIN) }
  0         0  
  0         0  
400 0         0 last;
401             };
402 1         4 close(_PIPE_WFH);
403 1         4 $saved_out = 1;
404             }
405              
406             # --- run the segment ---
407 2         18 $rc = _exec_single($class, $seg, $lines_ref, $labels_ref, $i_ref, $opts_ref);
408              
409             # --- restore STDOUT ---
410 2 100       12 if ($saved_out) {
411 1         70 open(STDOUT, '>&_PIPE_SAVOUT');
412 1         6 close(_PIPE_SAVOUT);
413             }
414              
415             # --- restore STDIN and clean up input tmpfile ---
416 2 100       8 if ($saved_in) {
417 1         52 open(STDIN, '<&_PIPE_SAVIN');
418 1         10 close(_PIPE_SAVIN);
419 1         262 unlink $input_f;
420             }
421              
422 2         22 $input_f = $output_f; # next segment reads what we just wrote
423             }
424              
425             # Clean up any leftover tmpfile (e.g. if last segment was skipped)
426 1 50 33     14 unlink $input_f if defined $input_f && -f $input_f;
427              
428 1         46 return $rc;
429             }
430              
431             # ----------------------------------------------------------------
432             # _strip_redirects: parse > >> 2> 2>> < from end of command
433             # Returns ($clean_cmd, \@redirs) where @redirs = ([fd,mode,file], ...)
434             # ----------------------------------------------------------------
435             sub _strip_redirects {
436 160     160   223 my ($line) = @_;
437 160         155 my @redirs;
438             # Parse redirects while respecting ^ escapes and quotes.
439             # A > or < preceded by ^ is NOT a redirect.
440             # Strategy: walk char-by-char to find bare (unescaped, unquoted) redirects.
441 160         718 my @chars = split //, $line;
442 160         197 my $n = scalar @chars;
443 160         204 my ($in_q, $i) = (0, 0);
444 160         180 my $clean = '';
445 160         232 my @found; # [pos_in_clean, fd, append, file_str]
446              
447 160         233 while ($i < $n) {
448 2991         2960 my $ch = $chars[$i];
449 2991 100 66     3897 if ($ch eq '^' && !$in_q) {
450             # Escape: pass through both ^ and next char as literals
451 3         4 $clean .= $ch;
452 3         3 $i++;
453 3 50       4 $clean .= $chars[$i] if $i < $n;
454 3         3 $i++;
455 3         6 next;
456             }
457 2988 100       3549 if ($ch eq '"') { $in_q = !$in_q; $clean .= $ch; $i++; next }
  44         50  
  44         43  
  44         44  
  44         66  
458 2944 100 66     7209 if (!$in_q && ($ch eq '>' || $ch eq '<')) {
      66        
459 3         32 my $fd = 1;
460 3 50       6 my $is_in = ($ch eq '<') ? 1 : 0;
461             # Check if the character immediately before (in clean, ignoring trailing space)
462             # is a bare fd digit that is not part of a word.
463             # Only '2' (stderr) and '1' (stdout explicit) are valid fd numbers in cmd.exe.
464             # We accept N> only if N is a single digit preceded by space/start-of-string.
465 3 50       17 if ($clean =~ s/(?:\A|(?<=[ \t]))([12])[ \t]*\z//) {
466 0         0 $fd = int($1);
467             }
468 3         8 my $append = 0;
469 3         4 $i++;
470 3 100 33     11 if (!$is_in && $i < $n && $chars[$i] eq '>') { $append = 1; $i++ }
  1   66     2  
  1         2  
471             # Skip whitespace before filename
472 3   66     18 $i++ while $i < $n && $chars[$i] =~ /[ \t]/;
473             # Read filename (until space/tab or end)
474 3         3 my $file = '';
475 3 50 33     13 if ($i < $n && $chars[$i] eq '"') {
476 0         0 $i++;
477 0   0     0 while ($i < $n && $chars[$i] ne '"') { $file .= $chars[$i++] }
  0         0  
478 0         0 $i++; # closing "
479             }
480             else {
481 3   66     10 while ($i < $n && $chars[$i] !~ /[ \t]/) { $file .= $chars[$i++] }
  72         125  
482             }
483 3 50       19 push @found, [$is_in ? 0 : $fd, $append, $file];
484 3         8 next;
485             }
486 2941         2997 $clean .= $ch; $i++;
  2941         3519  
487             }
488 160         503 $clean =~ s/\s+\z//;
489 160         657 return ($clean, \@found);
490             }
491              
492             # ----------------------------------------------------------------
493             # _dispatch_with_redirs: set up redirections then dispatch
494             # Perl 5.005_03 compatible: fixed bareword FHs, 2-argument open.
495             # ----------------------------------------------------------------
496              
497             # Fixed bareword filehandles used only inside _dispatch_with_redirs.
498             # Pre-declared at package level so they are valid under strict.
499 7     7   58 use vars qw(*_REDIR_SRC *_REDIR_DST *_REDIR_SAVOUT *_REDIR_SAVERR *_REDIR_SAVIN);
  7         55  
  7         80932  
500              
501             sub _dispatch_with_redirs {
502 160     160   337 my ($class, $line, $redirs, $lines_ref, $labels_ref, $i_ref, $opts_ref) = @_;
503              
504             return _dispatch($class, $line, $lines_ref, $labels_ref, $i_ref, $opts_ref)
505 160 100       184 unless @{$redirs};
  160         428  
506              
507             # Process redirections one at a time using fixed bareword FHs.
508             # We support one redirect per fd (last one wins, matching cmd.exe).
509             # Collect: stdin_file, stdout_file, stdout_append, stderr_file, stderr_append
510 3         5 my ($in_file, $out_file, $out_app, $err_file, $err_app);
511 3         2 for my $r (@{$redirs}) {
  3         20  
512 3         3 my ($fd, $append, $file) = @{$r};
  3         4  
513 3 50       8 if ($fd == 0) { $in_file = $file }
  0 50       0  
514 3         2 elsif ($fd == 1) { $out_file = $file; $out_app = $append }
  3         5  
515 0         0 else { $err_file = $file; $err_app = $append }
  0         0  
516             }
517              
518 3         2 my $ok = 1;
519 3         4 my ($saved_in, $saved_out, $saved_err) = (0, 0, 0);
520              
521             # stdin
522 3 50 33     7 if (defined $in_file && $ok) {
523 0 0       0 open(_REDIR_SRC, $in_file) or do { _warn("Cannot open $in_file: $!"); $ok=0 };
  0         0  
  0         0  
524 0 0       0 if ($ok) {
525 0 0       0 open(_REDIR_SAVIN, '<&STDIN') or do { $ok=0 };
  0         0  
526             }
527 0 0       0 if ($ok) {
528 0 0       0 open(STDIN, '<&_REDIR_SRC') or do { $ok=0 };
  0         0  
529 0         0 close(_REDIR_SRC);
530 0         0 $saved_in = 1;
531             }
532             }
533              
534             # stdout
535 3 50 33     8 if (defined $out_file && $ok) {
536 3 100       12 my $mode = $out_app ? '>>' : '>';
537 3 50       343 open(_REDIR_DST, "$mode$out_file") or do { _warn("Cannot open $out_file: $!"); $ok=0 };
  0         0  
  0         0  
538 3 50       9 if ($ok) {
539 3 50       32 open(_REDIR_SAVOUT, '>&STDOUT') or do { $ok=0 };
  0         0  
540             }
541 3 50       3 if ($ok) {
542 3 50       36 open(STDOUT, '>&_REDIR_DST') or do { $ok=0 };
  0         0  
543 3         8 close(_REDIR_DST);
544 3         4 $saved_out = 1;
545             }
546             }
547              
548             # stderr
549 3 50 33     7 if (defined $err_file && $ok) {
550 0 0       0 my $mode = $err_app ? '>>' : '>';
551 0 0       0 open(_REDIR_DST, "$mode$err_file") or do { _warn("Cannot open $err_file: $!"); $ok=0 };
  0         0  
  0         0  
552 0 0       0 if ($ok) {
553 0 0       0 open(_REDIR_SAVERR, '>&STDERR') or do { $ok=0 };
  0         0  
554             }
555 0 0       0 if ($ok) {
556 0 0       0 open(STDERR, '>&_REDIR_DST') or do { $ok=0 };
  0         0  
557 0         0 close(_REDIR_DST);
558 0         0 $saved_err = 1;
559             }
560             }
561              
562 3         11 my $rc = 0;
563 3 50       9 $rc = _dispatch($class, $line, $lines_ref, $labels_ref, $i_ref, $opts_ref) if $ok;
564              
565             # Restore in reverse order
566 3 50       9 if ($saved_err) { open(STDERR, '>&_REDIR_SAVERR'); close(_REDIR_SAVERR) }
  0         0  
  0         0  
567 3 50       6 if ($saved_out) { open(STDOUT, '>&_REDIR_SAVOUT'); close(_REDIR_SAVOUT) }
  3         141  
  3         13  
568 3 50       5 if ($saved_in) { open(STDIN, '<&_REDIR_SAVIN'); close(_REDIR_SAVIN) }
  0         0  
  0         0  
569              
570 3         13 return $rc;
571             }
572              
573             # ----------------------------------------------------------------
574             # Command dispatcher
575             # ----------------------------------------------------------------
576             sub _dispatch {
577 160     160   253 my ($class, $line, $lines_ref, $labels_ref, $i_ref, $opts_ref) = @_;
578              
579 160         261 my ($cmd, $rest) = _split_cmd($line);
580 160 50 33     454 return 0 unless defined $cmd && $cmd ne '';
581              
582 160         222 my $CMD = uc($cmd);
583              
584 160 100       212 if ($CMD eq 'ECHO') { return _cmd_echo($rest) }
  47         85  
585 113 50       148 if ($CMD eq '@ECHO') { return _cmd_echo($rest) }
  0         0  
586 113 100       147 if ($CMD eq 'SET') { return _cmd_set($rest) }
  66         97  
587 47 100       82 if ($CMD eq 'IF') { return _cmd_if($class, $line, $lines_ref, $labels_ref, $i_ref, $opts_ref) }
  15         38  
588 32 100       46 if ($CMD eq 'FOR') { return _cmd_for($class, $line, $lines_ref, $labels_ref, $i_ref, $opts_ref) }
  11         28  
589 21 100       37 if ($CMD eq 'GOTO') {
590 1         2 $rest =~ s/\A\s+//; $rest =~ s/\s+\z//; $rest =~ s/\A://;
  1         2  
  1         3  
591 1         2 $_GOTO_LABEL = uc($rest);
592 1         3 return 0;
593             }
594 20 50       40 if ($CMD eq 'CALL') { return _cmd_call($class, $rest, $opts_ref) }
  0         0  
595 20 100       35 if ($CMD eq 'SETLOCAL') {
596 8         11 $rest =~ s/\A\s+//; $rest =~ s/\s+\z//;
  8         13  
597 8         23 BATsh::Env::setlocal($rest);
598 8         27 return 0;
599             }
600 12 100       25 if ($CMD eq 'ENDLOCAL') { BATsh::Env::endlocal(); return 0 }
  8         21  
  8         33  
601 4 50 33     20 if ($CMD eq 'CD' || $CMD eq 'CHDIR') { return _cmd_cd($rest) }
  0         0  
602 4 50       6 if ($CMD eq 'DIR') { return _cmd_dir($rest) }
  0         0  
603 4 50       7 if ($CMD eq 'COPY') { return _cmd_copy($rest) }
  0         0  
604 4 50 33     12 if ($CMD eq 'DEL' || $CMD eq 'ERASE') { return _cmd_del($rest) }
  0         0  
605 4 50       8 if ($CMD eq 'MOVE') { return _cmd_move($rest) }
  0         0  
606 4 50 33     14 if ($CMD eq 'MKDIR' || $CMD eq 'MD') { return _cmd_mkdir($rest) }
  0         0  
607 4 50 33     18 if ($CMD eq 'RMDIR' || $CMD eq 'RD') { return _cmd_rmdir($rest) }
  0         0  
608 4 50 33     10 if ($CMD eq 'REN' || $CMD eq 'RENAME') { return _cmd_rename($rest) }
  0         0  
609 4 100       14 if ($CMD eq 'TYPE') { return _cmd_type($rest) }
  3         7  
610 1 50       2 if ($CMD eq 'PAUSE') {
611 0         0 print "Press any key to continue . . . ";
612 0         0 my $ch = '';
613 0         0 eval { local $| = 1; require POSIX; POSIX::tcgetattr(0); read(STDIN, $ch, 1) };
  0         0  
  0         0  
  0         0  
  0         0  
614 0         0 print "\n";
615 0         0 return 0;
616             }
617 1 50       2 if ($CMD eq 'EXIT') {
618 0         0 $rest =~ s/\A\s+//;
619 0 0       0 my $is_b = ($rest =~ s{/B\s*}{}i) ? 1 : 0;
620 0         0 $rest =~ s/\A\s+//;
621 0 0       0 $ERRORLEVEL = ($rest =~ /\A\d+\z/) ? int($rest) : 0;
622 0         0 return '__EXIT__';
623             }
624 1 50       3 if ($CMD eq 'CLS') { print "\033[2J\033[H"; return 0 }
  0         0  
  0         0  
625 1 50       3 if ($CMD eq 'TITLE') { print "\033]0;$rest\007"; return 0 }
  0         0  
  0         0  
626 1 50       2 if ($CMD eq 'VER') { print "BATsh [Version $BATsh::VERSION]\n"; return 0 }
  0         0  
  0         0  
627 1 50       3 if ($CMD eq 'PUSHD') {
628 0         0 $rest =~ s/\A\s+//; $rest =~ s/\s+\z//;
  0         0  
629 0         0 push @{$opts_ref->{'_pushd_stack'}}, Cwd::cwd();
  0         0  
630 0         0 return _cmd_cd($rest);
631             }
632 1 50       2 if ($CMD eq 'POPD') {
633 0 0 0     0 if (defined $opts_ref->{'_pushd_stack'} && @{$opts_ref->{'_pushd_stack'}}) {
  0         0  
634 0         0 chdir(pop @{$opts_ref->{'_pushd_stack'}});
  0         0  
635             }
636 0         0 return 0;
637             }
638              
639 1         3 return _cmd_external($cmd, $rest);
640             }
641              
642             # ----------------------------------------------------------------
643             # ECHO (does NOT reset ERRORLEVEL -- cmd.exe compatible)
644             # ----------------------------------------------------------------
645             sub _cmd_echo {
646 47     47   64 my ($rest) = @_;
647 47         70 $rest =~ s/\A\s+//;
648              
649 47 100       96 if ($rest =~ /\AOFF\s*\z/i) { $ECHO_ON = 0; return 0; }
  1         2  
  1         3  
650 46 50       86 if ($rest =~ /\AON\s*\z/i) { $ECHO_ON = 1; return 0; }
  0         0  
  0         0  
651 46 50       75 if ($rest =~ /\A\.\s*\z/) { print "\n"; return 0; }
  0         0  
  0         0  
652 46 0       180 if ($rest =~ /\A\s*\z/) { print "ECHO is " . ($ECHO_ON ? "on" : "off") . "\n"; return 0; }
  0 50       0  
  0         0  
653              
654             # Remove ^ escapes for display (they were protection, not content)
655 46         65 $rest = _unescape_caret($rest);
656 46         102 print "$rest\n";
657             # ERRORLEVEL intentionally NOT modified here
658 46         130 return 0;
659             }
660              
661             # ----------------------------------------------------------------
662             # SET
663             # ----------------------------------------------------------------
664             sub _cmd_set {
665 66     66   84 my ($rest) = @_;
666 66         115 $rest =~ s/\A\s+//;
667              
668             # SET /P VAR=PromptString (interactive prompt input)
669 66 100       111 if ($rest =~ s/\A\/P\s*//i) {
670 3 50       12 if ($rest =~ /\A([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)/) {
671 3         5 my ($var, $prompt) = ($1, $2);
672 3         7 print $prompt;
673 3         92 my $input = ;
674 3 50       7 $input = '' unless defined $input;
675 3         5 chomp $input;
676 3         12 BATsh::Env->set($var, $input);
677 3         5 $ERRORLEVEL = 0;
678             }
679 3         12 return 0;
680             }
681              
682             # SET /A
683 63 100       124 if ($rest =~ s/\A\/A\s*//i) {
684 15 50       57 if ($rest =~ /\A\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)/) {
685 15         27 BATsh::Env->set($1, _eval_arith($2));
686             }
687             else {
688 0         0 print _eval_arith($rest) . "\n";
689             }
690 15         18 $ERRORLEVEL = 0;
691 15         48 return 0;
692             }
693              
694             # SET with no args: display all
695 48 50       84 if ($rest =~ /\A\s*\z/) {
696 0         0 for my $k (sort keys %BATsh::Env::STORE) {
697 0         0 print "$k=$BATsh::Env::STORE{$k}\n";
698             }
699 0         0 return 0;
700             }
701              
702             # SET VAR=value (variable name may contain spaces before =)
703 48 50       201 if ($rest =~ /\A([^=]+?)\s*=(.*)/) {
704 48         151 BATsh::Env->set($1, $2);
705 48         55 $ERRORLEVEL = 0;
706 48         139 return 0;
707             }
708              
709             # SET VAR (display matching prefix)
710 0 0       0 if ($rest =~ /\A(\S+)\s*\z/) {
711 0         0 my $prefix = uc($1);
712 0         0 for my $k (sort keys %BATsh::Env::STORE) {
713 0 0       0 if (index(uc($k), $prefix) == 0) {
714 0         0 print "$k=$BATsh::Env::STORE{$k}\n";
715             }
716             }
717 0         0 return 0;
718             }
719              
720 0         0 return 0;
721             }
722              
723             # ----------------------------------------------------------------
724             # SET /A arithmetic evaluator
725             # Supports: + - * / % ^ & | ~ << >> () hex (0x) and variable refs
726             # ----------------------------------------------------------------
727             sub _eval_arith {
728 15     15   65 my ($expr) = @_;
729             # Expand variable names
730 15         30 $expr =~ s/([A-Za-z_][A-Za-z0-9_]*)/
731 4 50 33     3 do { my $v = BATsh::Env->get($1); defined $v && $v =~ m|^-?\d+$| ? $v : 0 }
  4         11  
  4         24  
732             /ge;
733             # Convert 0xHEX literals
734 15         20 $expr =~ s/0x([0-9A-Fa-f]+)/hex($1)/ge;
  0         0  
735             # %% -> % (modulo)
736 15         58 $expr =~ s/%%/%/g;
737             # Safe eval: digits, operators, hex chars already substituted
738 15 50       43 if ($expr =~ /\A[\d\s\+\-\*\/\%\(\)\^\&\|\~\<\>]+\z/) {
739             # Perl ^ is XOR, same as cmd.exe SET /A
740 15         835 my $result = eval $expr;
741 15 50       89 return defined $result ? int($result) : 0;
742             }
743 0         0 return 0;
744             }
745              
746             # ----------------------------------------------------------------
747             # IF
748             #
749             # cmd.exe parse order:
750             # IF [NOT] /I "a"=="b" ... (case-insensitive string)
751             # IF [NOT] ERRORLEVEL n ...
752             # IF [NOT] EXIST path ...
753             # IF [NOT] DEFINED var ...
754             # IF [NOT] "a"=="b" ... (case-sensitive string)
755             #
756             # IMPORTANT: /I must be checked BEFORE plain == to avoid /I being
757             # consumed as part of the left-hand operand.
758             #
759             # Block expansion: the THEN/ELSE bodies of a parenthesised IF block
760             # are expanded at parse time (before any SET inside runs), matching
761             # cmd.exe's behaviour. Only !VAR! (delayed) is deferred to runtime.
762             # ----------------------------------------------------------------
763             sub _cmd_if {
764 15     15   30 my ($class, $line, $lines_ref, $labels_ref, $i_ref, $opts_ref) = @_;
765              
766 15         58 (my $rest = $line) =~ s/\AIF\s+//i;
767              
768 15         21 my $negate = 0;
769 15 100       38 if ($rest =~ s/\ANOT\s+//i) { $negate = 1; }
  2         5  
770              
771 15         17 my $condition = 0;
772              
773             # /I must be tried first
774 15 100       117 if ($rest =~ s/\A\/I\s+//i) {
    100          
    100          
    100          
    50          
775             # Case-insensitive comparison
776 1 50       8 if ($rest =~ s/\A("(?:[^"]*)"|[^\s=][^\s=]*)\s*==\s*("(?:[^"]*)"|[^\s=]*)\s*//) {
777 1         3 my ($a, $b) = ($1, $2);
778 1         4 $a =~ s/\A"//; $a =~ s/"\z//;
  1         3  
779 1         3 $b =~ s/\A"//; $b =~ s/"\z//;
  1         3  
780 1 50       4 $condition = (lc($a) eq lc($b)) ? 1 : 0;
781             }
782             }
783             # ERRORLEVEL n
784             elsif ($rest =~ s/\AERRORLEVEL\s+(\d+)\s*//i) {
785 3 100       12 $condition = ($ERRORLEVEL >= int($1)) ? 1 : 0;
786             }
787             # EXIST path (handles quoted paths with spaces)
788             elsif ($rest =~ s/\AEXIST\s+//i) {
789 2         75 my $path;
790 2 100       16 if ($rest =~ s/\A"([^"]+)"\s*//) {
    50          
791 1         3 $path = $1;
792             }
793             elsif ($rest =~ s/\A(\S+)\s*//) {
794 1         2 $path = $1;
795             }
796 2 50 33     51 $condition = (defined $path && -e $path) ? 1 : 0;
797             }
798             # DEFINED var
799             elsif ($rest =~ s/\ADEFINED\s+(\S+)\s*//i) {
800 1 50       6 $condition = BATsh::Env->exists_var($1) ? 1 : 0;
801             }
802             # "str"=="str" or str==str (case-sensitive)
803             elsif ($rest =~ s/\A("(?:[^"]*)"|[^\s=][^\s=]*)\s*==\s*("(?:[^"]*)"|[^\s=]*)\s*//) {
804 8         19 my ($a, $b) = ($1, $2);
805 8         16 $a =~ s/\A"//; $a =~ s/"\z//;
  8         31  
806 8         13 $b =~ s/\A"//; $b =~ s/"\z//;
  8         14  
807 8 100       19 $condition = ($a eq $b) ? 1 : 0;
808             }
809              
810 15 100       32 $condition = !$condition if $negate;
811              
812 15         32 my ($then_body, $else_body) = _parse_if_bodies($rest, $lines_ref, $i_ref);
813              
814             # Block expansion: expand %VAR% in the bodies NOW (parse-time),
815             # before any commands inside the block execute.
816             # !VAR! is NOT expanded here (that is deferred to execute-time via Env).
817 15 50       45 $then_body = _block_expand($then_body) if defined $then_body;
818 15 100       36 $else_body = _block_expand($else_body) if defined $else_body;
819              
820 15 100       31 if ($condition) {
    100          
821 11         24 return _exec_body($class, $then_body, $lines_ref, $labels_ref, $i_ref, $opts_ref, 1);
822             }
823             elsif (defined $else_body) {
824 2         5 return _exec_body($class, $else_body, $lines_ref, $labels_ref, $i_ref, $opts_ref, 1);
825             }
826 2         14 return 0;
827             }
828              
829             # ----------------------------------------------------------------
830             # _block_expand: expand %VAR% in a block string at parse time.
831             # Protects %%V FOR loop variables and !VAR! delayed references.
832             # ----------------------------------------------------------------
833             sub _block_expand {
834 19     19   30 my ($body) = @_;
835 19 50       34 return $body unless defined $body;
836             # Protect !VAR! -- replace with placeholder to survive %% pass
837 19         32 $body =~ s/(!(?:[A-Za-z_][A-Za-z0-9_]*)!)/"\x00DELAY\x00$1\x00DELAY\x00"/ge;
  2         9  
838             # Protect %%V
839 19         25 $body =~ s/%%([A-Za-z])/"\x00FOR_$1\x00"/ge;
  0         0  
840             # Expand %VAR%
841 19         39 $body =~ s/%([^%\r\n]+)%/
842 3 50       3 do { my $k=uc($1); exists($BATsh::Env::STORE{$k}) ? $BATsh::Env::STORE{$k} : '' }
  3         7  
  3         10  
843             /ge;
844             # %% -> %
845 19         29 $body =~ s/%%/%/g;
846             # Restore
847 19         26 $body =~ s/\x00FOR_([A-Za-z])\x00/%%$1/g;
848 19         43 $body =~ s/\x00DELAY\x00(!(?:[A-Za-z_][A-Za-z0-9_]*)!)\x00DELAY\x00/$1/g;
849 19         29 return $body;
850             }
851              
852             sub _parse_if_bodies {
853 15     15   59 my ($rest, $lines_ref, $i_ref) = @_;
854 15         32 my ($then_body, $else_body);
855 15         30 $rest =~ s/\A\s+//;
856 15 100       42 if ($rest =~ s/\A\(//) {
857 4         35 $then_body = _read_paren_block($rest, $lines_ref, $i_ref, \$else_body);
858             }
859             else {
860 11 50       56 if ($rest =~ s/\s+ELSE\s+(.+)\z//i) { $else_body = $1 }
  0         0  
861 11         16 $then_body = $rest;
862             }
863 15         42 return ($then_body, $else_body);
864             }
865              
866             sub _read_paren_block {
867 8     8   17 my ($first_content, $lines_ref, $i_ref, $else_ref) = @_;
868 8         14 my @body = ();
869 8 50 33     30 push @body, $first_content if defined $first_content && $first_content =~ /\S/;
870 8         10 my $depth = 1;
871 8         14 while ($$i_ref <= $#{$lines_ref}) {
  21         28  
872 21         37 my $l = $lines_ref->[$$i_ref];
873 21         22 $$i_ref++;
874 21         25 $l =~ s/\r?\n\z//;
875 21         21 my $ls = $l; $ls =~ s/\A\s+//;
  21         42  
876 21 100 66     64 if ($depth == 1 && $ls =~ /\A\)\s*ELSE\s*\(\s*\z/i) {
877 2 50       6 if (defined $else_ref) { $$else_ref = _read_paren_block('', $lines_ref, $i_ref) }
  2         9  
878 2         5 last;
879             }
880 19 50 33     57 if ($depth == 1 && $ls =~ /\A\)\s*ELSE\s+(.+)\z/i) {
881 0 0       0 if (defined $else_ref) { $$else_ref = $1 }
  0         0  
882 0         0 last;
883             }
884 19         21 my ($delta, $in_q) = (0, 0);
885 19         51 for my $ch (split //, $l) {
886 207 50       266 if ($ch eq '"') { $in_q = !$in_q }
  0 50       0  
887 207 50       249 elsif (!$in_q) { $delta++ if $ch eq '('; $delta-- if $ch eq ')' }
  207 100       247  
888             }
889 19         38 $depth += $delta;
890 19 100       24 if ($depth <= 0) {
891 6         16 $l =~ s/\)\s*\z//;
892 6 50       14 push @body, $l if $l =~ /\S/;
893 6         11 last;
894             }
895 13         15 push @body, $l;
896             }
897 8         30 return join("\n", @body);
898             }
899              
900             sub _exec_body {
901 19     19   35 my ($class, $body, $lines_ref, $labels_ref, $i_ref, $opts_ref, $pre_expanded) = @_;
902 19 50 33     112 return 0 unless defined $body && $body =~ /\S/;
903 19 50       28 $pre_expanded = 0 unless defined $pre_expanded;
904 19         43 my @sub_lines = split /\n/, $body;
905 19         23 my $sub_i = 0;
906 19         27 my %sub_labels = ();
907 19         43 for my $j (0 .. $#sub_lines) {
908 28         39 my $ls = $sub_lines[$j]; $ls =~ s/\A\s+//;
  28         42  
909 28 50       74 if ($ls =~ /\A:([A-Za-z_][A-Za-z0-9_]*)\s*\z/) {
910 0         0 $sub_labels{uc($1)} = $j;
911             }
912             }
913 19         34 while ($sub_i <= $#sub_lines) {
914 28         32 my $sl = $sub_lines[$sub_i];
915 28         29 $sub_i++;
916             # For pre_expanded blocks: still need to handle !VAR! at runtime
917 28         81 my $rc = _exec_line($class, $sl, \@sub_lines, \%sub_labels, \$sub_i, $opts_ref, $pre_expanded);
918 28 50 33     97 return $rc if defined $rc && $rc eq '__EXIT__';
919 28 50       65 if ($_GOTO_LABEL ne '') {
920 0         0 my $lbl = $_GOTO_LABEL;
921 0         0 $_GOTO_LABEL = '';
922 0 0       0 if (exists $sub_labels{$lbl}) {
923 0         0 $sub_i = $sub_labels{$lbl} + 1;
924             }
925             else {
926 0         0 $_GOTO_LABEL = $lbl;
927 0         0 return 0;
928             }
929             }
930             }
931 19         82 return 0;
932             }
933              
934             # ----------------------------------------------------------------
935             # FOR
936             # ----------------------------------------------------------------
937             sub _cmd_for {
938 11     11   20 my ($class, $line, $lines_ref, $labels_ref, $i_ref, $opts_ref) = @_;
939              
940             # FOR /F "options" %%V IN (source) DO cmd
941 11 100       61 if ($line =~ /\AFOR\s+\/F\s+("(?:[^"]*)"|'(?:[^']*)'|[^\s]+)\s+(?:%%|\x00FOR_)([A-Za-z])(?:\x00)?\s+IN\s*\(([^)]*)\)\s+DO\s+(.*)/i) {
942 6         19 return _cmd_for_f($class, $1, $2, $3, $4, $lines_ref, $labels_ref, $i_ref, $opts_ref);
943             }
944              
945             # FOR /L %%V IN (start,step,end) DO cmd
946 5 100       23 if ($line =~ /\AFOR\s+\/L\s+(?:%%|\x00FOR_)([A-Za-z])(?:\x00)?\s+IN\s*\(([^)]*)\)\s+DO\s+(.*)/i) {
947 2         8 my ($var, $range, $do_part) = ($1, $2, $3);
948 2         7 my ($start, $step, $end) = split /,/, $range;
949 2 50       20 $start = defined $start ? int($start) : 0;
950 2 50       5 $step = defined $step ? int($step) : 1;
951 2 50       4 $end = defined $end ? int($end) : 0;
952 2 50       53 $step = 1 if $step == 0;
953             return _for_iterate($class, $var, $do_part, $lines_ref, $labels_ref, $i_ref, $opts_ref,
954             sub { # generator: returns list of values
955 2     2   12 my @vals;
956 2         3 my $v = $start;
957 2   66     16 while (($step > 0 && $v <= $end) || ($step < 0 && $v >= $end)) {
      33        
      66        
958 9         9 push @vals, $v;
959 9         24 $v += $step;
960             }
961 2         5 return @vals;
962 2         13 });
963             }
964              
965             # FOR %%V IN (list) DO cmd
966 3 50       20 if ($line =~ /\AFOR\s+(?:%%|\x00FOR_)([A-Za-z])(?:\x00)?\s+IN\s*\(([^)]*)\)\s+DO\s+(.*)/i) {
967 3         18 my ($var, $list_str, $do_part) = ($1, $2, $3);
968 3         11 my @items = split /[\s,]+/, $list_str;
969 3         3 my @expanded = ();
970 3         6 for my $item (@items) {
971 9         14 $item =~ s/\A\s+//; $item =~ s/\s+\z//;
  9         10  
972 9 50       19 next if $item eq '';
973 9 50       14 if ($item =~ /[*?]/) {
974 0         0 my @g = glob($item);
975 0 0       0 push @expanded, @g ? @g : ($item);
976             }
977 9         11 else { push @expanded, $item }
978             }
979             return _for_iterate($class, $var, $do_part, $lines_ref, $labels_ref, $i_ref, $opts_ref,
980 3     3   18 sub { return @expanded });
  3         6  
981             }
982              
983 0         0 _warn("FOR: unsupported syntax: $line");
984 0         0 return 1;
985             }
986              
987             # ----------------------------------------------------------------
988             # _for_iterate: common FOR loop body runner
989             # ----------------------------------------------------------------
990             sub _for_iterate {
991 5     5   13 my ($class, $var, $do_part, $lines_ref, $labels_ref, $i_ref, $opts_ref, $gen) = @_;
992              
993             # Pre-read paren body if do_part is "("
994 5         9 my $paren_body_template = undef;
995             {
996 5         6 my $probe = $do_part;
  5         15  
997 5         11 $probe =~ s/\x00FOR_[A-Za-z]\x00//g;
998 5         15 $probe =~ s/\x00PCT_[^\x00]+\x00//g;
999 5         7 $probe =~ s/%%[A-Za-z]//g;
1000 5 100       15 if ($probe =~ /\A\s*\(\s*\z/) {
1001 2         9 $paren_body_template = _read_paren_block('', $lines_ref, $i_ref);
1002             }
1003             }
1004              
1005             # If we have a paren block, expand %VAR% ONCE at FOR-parse time
1006             # (cmd.exe expands the whole block before any iteration runs).
1007             # PCT placeholders (%%V protected vars) are restored to %VAR% first,
1008             # then _block_expand runs the single-pass %VAR% substitution.
1009             # The result is a template with loop-var placeholders still intact.
1010 5         14 my $paren_expanded = undef;
1011 5 100       10 if (defined $paren_body_template) {
1012 2         3 my $tpl = $paren_body_template;
1013             # Restore PCT placeholders -> %VAR% so _block_expand can see them
1014 2         3 $tpl =~ s/\x00PCT_([^\x00]+)\x00/%$1%/g;
1015             # But protect the loop variable itself from _block_expand
1016             # (it will be substituted per-iteration below)
1017 2         21 $tpl =~ s/%%$var/"\x00LOOPVAR\x00"/ge;
  1         4  
1018 2         11 $tpl =~ s/\x00FOR_$var\x00/"\x00LOOPVAR\x00"/ge;
  0         0  
1019             # Single-pass %VAR% expansion at FOR-line parse time
1020 2         3 $paren_expanded = _block_expand($tpl);
1021             # _block_expand already restored other %%X -> %%X, leave loop placeholder
1022             }
1023              
1024 5         10 my @values = $gen->();
1025              
1026 5         16 for my $val (@values) {
1027 18         53 BATsh::Env->set("%%$var", $val);
1028              
1029 18 100       48 if (defined $paren_expanded) {
1030             # Substitute loop variable placeholder with current value
1031 6         6 my $body = $paren_expanded;
1032 6         11 $body =~ s/\x00LOOPVAR\x00/$val/g;
1033             # At runtime: if delayed expansion is on, expand !VAR!
1034             # _exec_body with pre_expanded=1 handles this via _exec_line
1035 6         9 _exec_body($class, $body, $lines_ref, $labels_ref, $i_ref, $opts_ref, 1);
1036             }
1037             else {
1038 12         16 my $do_line = $do_part;
1039             # Replace loop variable placeholder/shorthand with current value
1040 12         78 $do_line =~ s/%%$var/$val/g;
1041 12         56 $do_line =~ s/\x00FOR_$var\x00/$val/g;
1042             # Restore other FOR-variable placeholders to %%X form
1043 12         22 $do_line =~ s/\x00FOR_([A-Za-z])\x00/%%$1/g;
1044             # Restore %VAR% placeholders so expand_cmd can expand them
1045 12         70 $do_line =~ s/\x00PCT_([^\x00]+)\x00/%$1%/g;
1046 12         33 $do_line = BATsh::Env->expand_cmd($do_line);
1047 12         22 _exec_line($class, $do_line, $lines_ref, $labels_ref, $i_ref, $opts_ref, 1);
1048             }
1049 18 50       48 last if $_GOTO_LABEL ne '';
1050             }
1051 5         43 return 0;
1052             }
1053              
1054             # ----------------------------------------------------------------
1055             # FOR /F
1056             #
1057             # Options string (inside quotes): tokens= delims= skip= eol= usebackq
1058             # Source:
1059             # "filename" -- iterate lines of file (or usebackq: command output)
1060             # 'command' -- command output (or usebackq: literal filename)
1061             # ("string") -- tokenize the string itself
1062             # ----------------------------------------------------------------
1063             sub _cmd_for_f {
1064 6     6   32 my ($class, $opts_str, $var, $source_str, $do_part, $lines_ref, $labels_ref, $i_ref, $opts_ref) = @_;
1065              
1066             # Strip outer quotes from opts_str
1067 6         19 $opts_str =~ s/\A"//; $opts_str =~ s/"\z//;
  6         14  
1068              
1069             # Parse options
1070 6         8 my $tokens_spec = '1'; # default: first token only
1071 6         8 my $delims = " \t"; # default delimiters
1072 6         7 my $skip = 0;
1073 6         8 my $eol = ';'; # default: skip lines starting with ;
1074 6         6 my $usebackq = 0;
1075              
1076 6 50       12 $usebackq = 1 if $opts_str =~ /usebackq/i;
1077 6 100       17 if ($opts_str =~ /tokens=(\S+)/i) {
1078 4         7 $tokens_spec = $1;
1079 4         16 $tokens_spec =~ s/,\z//;
1080             }
1081 6 100       19 if ($opts_str =~ /delims=([^\s"]*)/i) {
    50          
1082 2         4 $delims = $1;
1083 2 50       7 $delims = ' ' if $delims eq ''; # delims= (empty) means no split? No: empty = space only
1084             }
1085             elsif ($opts_str =~ /delims=\s*\z/i) {
1086 0         0 $delims = ''; # delims= with nothing = no delimiter (whole line = one token)
1087             }
1088 6 100       14 if ($opts_str =~ /skip=(\d+)/i) { $skip = int($1) }
  1         2  
1089 6 100       12 if ($opts_str =~ /eol=(.)/i) { $eol = $1 }
  1         3  
1090              
1091             # Parse tokens spec: e.g. "1,2,3" "1-3" "1,2*" "*"
1092 6         10 my @token_indices = _parse_tokens_spec($tokens_spec);
1093 6 100       15 my $want_star = ($tokens_spec =~ /\*/) ? 1 : 0;
1094              
1095             # Determine source lines
1096 6         6 my @lines_to_process;
1097 6         9 $source_str =~ s/\A\s+//; $source_str =~ s/\s+\z//;
  6         25  
1098              
1099 6 50 33     55 if ($source_str =~ /\A'([^']*)'\z/ || ($usebackq && $source_str =~ /\A`([^`]*)`\z/)) {
    50 33        
    100 33        
    50 66        
1100             # Command output
1101 0         0 my $cmd = $1;
1102 0         0 BATsh::Env->sync_to_env();
1103 0         0 local *CMDOUT;
1104 0 0       0 open(CMDOUT, "$cmd |") or return 1;
1105 0         0 @lines_to_process = ;
1106 0         0 close(CMDOUT);
1107             }
1108             elsif ($usebackq && $source_str =~ /\A"([^"]*)"\z/) {
1109             # usebackq: "..." = filename
1110 0         0 my $file = $1;
1111 0         0 local *FFH;
1112 0 0       0 open(FFH, $file) or do { _warn("FOR /F: cannot open $file"); return 1 };
  0         0  
  0         0  
1113 0         0 @lines_to_process = ;
1114 0         0 close(FFH);
1115             }
1116             elsif ($source_str =~ /\A"([^"]*)"\z/ && !$usebackq) {
1117             # No usebackq: "string" = literal string to tokenize
1118 3         10 @lines_to_process = ("$1\n");
1119             }
1120             elsif ($source_str =~ /\A(\S+)\z/) {
1121             # Bare filename
1122 3         6 my $file = $1;
1123 3         10 local *FFH2;
1124 3 50       123 open(FFH2, $file) or do { _warn("FOR /F: cannot open $file"); return 1 };
  0         0  
  0         0  
1125 3         100 @lines_to_process = ;
1126 3         37 close(FFH2);
1127             }
1128             else {
1129 0         0 _warn("FOR /F: cannot parse source: $source_str");
1130 0         0 return 1;
1131             }
1132              
1133             # Skip leading lines
1134 6 100       11 splice(@lines_to_process, 0, $skip) if $skip > 0;
1135              
1136             # Pre-read paren body if needed
1137 6         11 my $paren_body = undef;
1138             {
1139 6         9 my $probe = $do_part;
  6         9  
1140 6         10 $probe =~ s/%%[A-Za-z]//g;
1141 6 50       16 if ($probe =~ /\A\s*\(\s*\z/) {
1142 0         0 $paren_body = _read_paren_block('', $lines_ref, $i_ref);
1143             }
1144             }
1145              
1146             # Determine variable names: %%a and following letters for extra tokens
1147 6         7 my @var_names;
1148 6         17 for my $i (0 .. $#token_indices) {
1149 8         24 push @var_names, chr(ord($var) + $i);
1150             }
1151             # Star token goes to the next letter after the listed ones
1152 6         11 my $star_var = chr(ord($var) + scalar @token_indices);
1153              
1154 6         8 for my $src_line (@lines_to_process) {
1155 8         33 $src_line =~ s/\r?\n\z//;
1156             # Skip eol lines
1157 8 100 66     123 next if $eol ne '' && $src_line =~ /\A\Q$eol\E/;
1158 7 50       18 next if $src_line =~ /\A\s*\z/;
1159              
1160             # Tokenize
1161 7         7 my @tokens;
1162 7 50       15 if ($delims eq '') {
1163 0         0 @tokens = ($src_line);
1164             }
1165             else {
1166 7         10 my $escaped_delims = quotemeta($delims);
1167 7         68 @tokens = split /[$escaped_delims]+/, $src_line;
1168             # cmd.exe skips leading delimiters
1169 7 50       55 if ($src_line =~ /\A[$escaped_delims]/) {
1170 0 0 0     0 shift @tokens if @tokens && $tokens[0] eq '';
1171             }
1172             }
1173              
1174             # Assign to variables
1175 7         15 for my $i (0 .. $#token_indices) {
1176 10         21 my $tidx = $token_indices[$i] - 1; # 0-based
1177 10 50       51 BATsh::Env->set("%%$var_names[$i]", defined $tokens[$tidx] ? $tokens[$tidx] : '');
1178             }
1179 7 100 66     19 if ($want_star && @tokens > $token_indices[-1]) {
1180             # Star: remainder from token N onwards joined by first delimiter
1181 1 50       5 my $delim1 = length($delims) > 0 ? substr($delims, 0, 1) : ' ';
1182 1         5 my $remainder = join($delim1, @tokens[$token_indices[-1] .. $#tokens]);
1183 1         3 BATsh::Env->set("%%$star_var", $remainder);
1184             }
1185              
1186             # Execute body
1187 7 50       12 if (defined $paren_body) {
1188 0         0 my $body = $paren_body;
1189             # Restore any \x00FOR_x\x00 placeholders before substituting values
1190 0         0 $body =~ s/\x00FOR_([A-Za-z])\x00/%%$1/g;
1191 0 0       0 for my $vn (@var_names, $want_star ? ($star_var) : ()) {
1192 0 0       0 my $val = defined(BATsh::Env->get("%%$vn")) ? BATsh::Env->get("%%$vn") : '';
1193 0         0 $body =~ s/%%$vn/$val/g;
1194             }
1195 0         0 $body = _block_expand($body);
1196 0         0 _exec_body($class, $body, $lines_ref, $labels_ref, $i_ref, $opts_ref, 1);
1197             }
1198             else {
1199 7         10 my $do_line = $do_part;
1200             # Restore \x00FOR_x\x00 -> %%x, then substitute values
1201 7         50 $do_line =~ s/\x00FOR_([A-Za-z])\x00/%%$1/g;
1202 7 100       30 for my $vn (@var_names, $want_star ? ($star_var) : ()) {
1203 11 50       28 my $val = defined(BATsh::Env->get("%%$vn")) ? BATsh::Env->get("%%$vn") : '';
1204 11         143 $do_line =~ s/%%$vn/$val/g;
1205             }
1206 7         17 $do_line = BATsh::Env->expand_cmd($do_line);
1207 7         16 _exec_line($class, $do_line, $lines_ref, $labels_ref, $i_ref, $opts_ref, 1);
1208             }
1209 7 50       25 last if $_GOTO_LABEL ne '';
1210             }
1211 6         36 return 0;
1212             }
1213              
1214             # ----------------------------------------------------------------
1215             # _parse_tokens_spec: "1,2,3-5,*" -> (1,2,3,4,5)
1216             # ----------------------------------------------------------------
1217             sub _parse_tokens_spec {
1218 6     6   11 my ($spec) = @_;
1219 6         9 $spec =~ s/\*//g; # star handled separately
1220 6         11 my @indices;
1221 6         17 for my $part (split /,/, $spec) {
1222 8         14 $part =~ s/\A\s+//; $part =~ s/\s+\z//;
  8         13  
1223 8 50       20 next unless $part =~ /\S/;
1224 8 50       27 if ($part =~ /\A(\d+)-(\d+)\z/) {
    50          
1225 0         0 push @indices, ($1 .. $2);
1226             }
1227             elsif ($part =~ /\A(\d+)\z/) {
1228 8         17 push @indices, $1;
1229             }
1230             }
1231 6 50       12 @indices = (1) unless @indices;
1232 6         16 return @indices;
1233             }
1234              
1235             # ----------------------------------------------------------------
1236             # CALL
1237             # ----------------------------------------------------------------
1238             sub _cmd_call {
1239 0     0   0 my ($class, $rest, $opts_ref) = @_;
1240 0         0 $rest =~ s/\A\s+//;
1241              
1242 0 0       0 if ($rest =~ /\A:([A-Za-z_][A-Za-z0-9_]*)(.*)/i) {
1243 0         0 my ($lbl, $argstr) = (uc($1), $2);
1244 0         0 $argstr =~ s/\A\s+//;
1245 0         0 my @args = split /\s+/, $argstr;
1246 0         0 for my $n (1 .. 9) {
1247 0 0       0 BATsh::Env->set("%$n", defined($args[$n-1]) ? $args[$n-1] : '');
1248             }
1249 0 0       0 if (defined $opts_ref->{'_batsh'}) {
1250 0         0 $opts_ref->{'_batsh'}->call_sub($lbl);
1251             }
1252 0         0 return 0;
1253             }
1254              
1255 0 0       0 if ($rest =~ /(\S+\.batsh)(.*)/i) {
1256 0         0 my $file = $1;
1257 0 0       0 if (defined $opts_ref->{'_batsh'}) {
1258 0         0 $opts_ref->{'_batsh'}->source_file($file);
1259             }
1260 0         0 return 0;
1261             }
1262              
1263             # CALL cmd args: execute with double-expansion
1264             # Re-expand the already-expanded string (second pass)
1265 0         0 my $reexpanded = BATsh::Env->expand_cmd($rest);
1266 0         0 return _cmd_external($reexpanded, '');
1267             }
1268              
1269             # ----------------------------------------------------------------
1270             # CD / CHDIR
1271             # ----------------------------------------------------------------
1272             sub _cmd_cd {
1273 0     0   0 my ($rest) = @_;
1274 0         0 $rest =~ s/\A\s+//; $rest =~ s/\s+\z//;
  0         0  
1275 0         0 $rest =~ s/\A"//; $rest =~ s/"\z//;
  0         0  
1276 0 0 0     0 if ($rest eq '' || $rest =~ /\A\/D\s*\z/i) {
1277 0         0 print Cwd::cwd(), "\n";
1278 0         0 return 0;
1279             }
1280 0         0 $rest =~ s/\A\/D\s*//i;
1281 0 0       0 unless (chdir($rest)) {
1282 0         0 print "The system cannot find the path specified.\n";
1283 0         0 $ERRORLEVEL = 1;
1284 0         0 return 1;
1285             }
1286 0         0 BATsh::Env->set('CD', Cwd::cwd());
1287 0         0 $ERRORLEVEL = 0;
1288 0         0 return 0;
1289             }
1290              
1291             # ----------------------------------------------------------------
1292             # DIR
1293             # ----------------------------------------------------------------
1294             sub _cmd_dir {
1295 0     0   0 my ($rest) = @_;
1296 0         0 $rest =~ s/\A\s+//; $rest =~ s/\s+\z//;
  0         0  
1297 0 0       0 my $target = $rest eq '' ? '.' : $rest;
1298 0         0 $target =~ s/\s*\/[A-Za-z:]+//g;
1299 0         0 $target =~ s/\s+\z//;
1300 0 0       0 $target = '.' if $target eq '';
1301 0         0 $target =~ s/\A"//; $target =~ s/"\z//;
  0         0  
1302 0 0       0 unless (-e $target) { print "File Not Found\n"; $ERRORLEVEL = 1; return 1 }
  0         0  
  0         0  
  0         0  
1303 0         0 local *DH;
1304 0 0       0 if (-d $target) {
1305 0 0       0 opendir(DH, $target) or do { print "Access denied.\n"; return 1 };
  0         0  
  0         0  
1306 0         0 my @entries = sort readdir(DH);
1307 0         0 closedir(DH);
1308 0         0 print " Directory of $target\n\n";
1309 0         0 for my $e (@entries) {
1310 0 0 0     0 next if $e eq '.' || $e eq '..';
1311 0         0 my $full = "$target/$e";
1312 0 0       0 if (-d $full) { printf "%-40s \n", $e }
  0         0  
1313 0         0 else { printf "%-40s %12d\n", $e, (-s $full) }
1314             }
1315             }
1316 0         0 else { printf "%-40s %12d\n", $target, (-s $target) }
1317 0         0 $ERRORLEVEL = 0;
1318 0         0 return 0;
1319             }
1320              
1321             # ----------------------------------------------------------------
1322             # File operations
1323             # ----------------------------------------------------------------
1324             sub _cmd_copy {
1325 0     0   0 my ($rest) = @_;
1326 0         0 $rest =~ s/\A\s+//; $rest =~ s/\s*\/[YN]\s*//gi;
  0         0  
1327 0         0 my ($src, $dst) = split /\s+/, $rest, 2;
1328 0 0 0     0 unless (defined $src && defined $dst) { print "The syntax of the command is incorrect.\n"; return 1 }
  0         0  
  0         0  
1329 0         0 $src =~ s/\A"//; $src =~ s/"\z//;
  0         0  
1330 0         0 $dst =~ s/\A"//; $dst =~ s/"\z//;
  0         0  
1331 0 0       0 unless (File::Copy::copy($src, $dst)) {
1332 0         0 print "The system cannot find the file specified.\n"; $ERRORLEVEL = 1; return 1
  0         0  
  0         0  
1333             }
1334 0         0 print " 1 file(s) copied.\n"; $ERRORLEVEL = 0; return 0;
  0         0  
  0         0  
1335             }
1336              
1337             sub _cmd_del {
1338 0     0   0 my ($rest) = @_;
1339 0         0 $rest =~ s/\A\s+//; $rest =~ s/\s*\/[A-Za-z:]+//g; $rest =~ s/\s+\z//;
  0         0  
  0         0  
1340 0         0 $rest =~ s/\A"//; $rest =~ s/"\z//;
  0         0  
1341 0         0 my @files = glob($rest);
1342 0 0       0 @files = ($rest) unless @files;
1343 0         0 for my $f (@files) {
1344 0 0       0 unlink($f) or print "Could not find $f\n";
1345             }
1346 0         0 $ERRORLEVEL = 0; return 0;
  0         0  
1347             }
1348              
1349             sub _cmd_move {
1350 0     0   0 my ($rest) = @_;
1351 0         0 $rest =~ s/\A\s+//; $rest =~ s/\s*\/[YN]\s*//gi;
  0         0  
1352 0         0 my ($src, $dst) = split /\s+/, $rest, 2;
1353 0 0 0     0 unless (defined $src && defined $dst) { print "The syntax of the command is incorrect.\n"; return 1 }
  0         0  
  0         0  
1354 0         0 $src =~ s/\A"//; $src =~ s/"\z//;
  0         0  
1355 0         0 $dst =~ s/\A"//; $dst =~ s/"\z//;
  0         0  
1356 0 0       0 unless (File::Copy::move($src, $dst)) {
1357 0         0 print "The system cannot find the file specified.\n"; $ERRORLEVEL = 1; return 1
  0         0  
  0         0  
1358             }
1359 0         0 print " 1 file(s) moved.\n"; $ERRORLEVEL = 0; return 0;
  0         0  
  0         0  
1360             }
1361              
1362             sub _cmd_mkdir {
1363 0     0   0 my ($rest) = @_;
1364 0         0 $rest =~ s/\A\s+//; $rest =~ s/\s+\z//; $rest =~ s/\A"//; $rest =~ s/"\z//;
  0         0  
  0         0  
  0         0  
1365 0 0       0 if (-d $rest) { print "A subdirectory or file $rest already exists.\n"; $ERRORLEVEL = 1; return 1 }
  0         0  
  0         0  
  0         0  
1366 0         0 File::Path::mkpath($rest); $ERRORLEVEL = 0; return 0;
  0         0  
  0         0  
1367             }
1368              
1369             sub _cmd_rmdir {
1370 0     0   0 my ($rest) = @_;
1371 0         0 $rest =~ s/\A\s+//;
1372 0 0       0 my $recurse = ($rest =~ s/\s*\/S\s*//i) ? 1 : 0;
1373 0         0 $rest =~ s/\s*\/Q\s*//i; $rest =~ s/\s+\z//; $rest =~ s/\A"//; $rest =~ s/"\z//;
  0         0  
  0         0  
  0         0  
1374 0 0       0 if ($recurse) { File::Path::rmtree($rest) }
  0         0  
1375             else {
1376 0 0       0 unless (rmdir($rest)) {
1377 0         0 print "The directory is not empty.\n"; $ERRORLEVEL = 1; return 1
  0         0  
  0         0  
1378             }
1379             }
1380 0         0 $ERRORLEVEL = 0; return 0;
  0         0  
1381             }
1382              
1383             sub _cmd_rename {
1384 0     0   0 my ($rest) = @_;
1385 0         0 $rest =~ s/\A\s+//;
1386 0         0 my ($src, $dst) = split /\s+/, $rest, 2;
1387 0 0 0     0 unless (defined $src && defined $dst) { print "The syntax of the command is incorrect.\n"; return 1 }
  0         0  
  0         0  
1388 0 0       0 unless (rename($src, $dst)) {
1389 0         0 print "Could not rename $src to $dst: $!\n"; $ERRORLEVEL = 1; return 1
  0         0  
  0         0  
1390             }
1391 0         0 $ERRORLEVEL = 0; return 0;
  0         0  
1392             }
1393              
1394             sub _cmd_type {
1395 3     3   8 my ($rest) = @_;
1396 3         5 $rest =~ s/\A\s+//; $rest =~ s/\s+\z//; $rest =~ s/\A"//; $rest =~ s/"\z//;
  3         3  
  3         4  
  3         4  
1397 3         8 local *TFH;
1398 3 50       76 unless (open(TFH, $rest)) {
1399 0         0 print "The system cannot find the file specified.\n"; $ERRORLEVEL = 1; return 1
  0         0  
  0         0  
1400             }
1401 3         43 while () { print }
  4         15  
1402 3         19 close(TFH);
1403 3         4 $ERRORLEVEL = 0; return 0;
  3         14  
1404             }
1405              
1406             # ----------------------------------------------------------------
1407             # External command
1408             # ----------------------------------------------------------------
1409             sub _cmd_external {
1410 1     1   3 my ($cmd, $rest) = @_;
1411 1 50       3 $rest = '' unless defined $rest;
1412 1         3 $rest =~ s/\A\s+//;
1413 1 50       4 my $full = $rest ne '' ? "$cmd $rest" : $cmd;
1414 1         2 $full = _unescape_caret($full);
1415 1         6 BATsh::Env->sync_to_env();
1416 1         556386 my $rc = system($full);
1417 1 50 0     57 $ERRORLEVEL = ($rc == 0) ? 0 : (($rc >> 8) || 1);
1418 1         60 return $ERRORLEVEL;
1419             }
1420              
1421             # ----------------------------------------------------------------
1422             # _split_cmd: split "COMMAND rest" respecting quotes
1423             # ----------------------------------------------------------------
1424             sub _split_cmd {
1425 160     160   247 my ($line) = @_;
1426 160 50       532 if ($line =~ /\A(\S+)\s*(.*)\z/s) {
1427 160         533 return ($1, $2);
1428             }
1429 0         0 return ($line, '');
1430             }
1431              
1432 0     0   0 sub _warn { print STDERR "[BATsh::CMD] $_[0]\n" }
1433              
1434             # ----------------------------------------------------------------
1435             # Accessors
1436             # ----------------------------------------------------------------
1437 1     1 0 257 sub errorlevel { return $ERRORLEVEL }
1438 2     2 0 10 sub set_errorlevel { $ERRORLEVEL = $_[1] }
1439 0     0 0   sub echo_on { return $ECHO_ON }
1440              
1441             BEGIN {
1442 7     7   58 eval { require Cwd };
  7         70  
1443 7 50       317 if ($@) {
1444 0         0 eval 'sub Cwd::cwd { return $ENV{CD} || "." }';
1445             }
1446             }
1447              
1448             1;
1449              
1450             __END__