File Coverage

lib/BATsh/CMD.pm
Criterion Covered Total %
statement 726 1055 68.8
branch 312 554 56.3
condition 67 167 40.1
subroutine 47 57 82.4
pod 0 4 0.0
total 1152 1837 62.7


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