File Coverage

lib/BATsh.pm
Criterion Covered Total %
statement 310 387 80.1
branch 150 222 67.5
condition 37 62 59.6
subroutine 26 33 78.7
pod 0 12 0.0
total 523 716 73.0


line stmt bran cond sub pod time code
1             package BATsh;
2             ######################################################################
3             #
4             # BATsh - Bilingual Shell for cmd.exe and bash in one script
5             #
6             # https://metacpan.org/dist/BATsh
7             #
8             # Copyright (c) 2026 INABA Hitoshi
9             #
10             # This version implements both cmd.exe and sh/bash command sets
11             # entirely in Perl. No external cmd.exe, bash, or sh is required.
12             #
13             ######################################################################
14              
15 15     15   2042115 use 5.00503;
  15         44  
16 15     15   54 use strict;
  15         27  
  15         1159  
17 15 50 33 15   387 BEGIN { if ($] < 5.006 && !defined(&warnings::import)) { $INC{'warnings.pm'} = 'stub'; eval 'package warnings; sub import {}' } }
  0         0  
  0         0  
18 15     15   58 use warnings; local $^W = 1;
  15         22  
  15         1106  
19 15 50   15   361 BEGIN { pop @INC if $INC[-1] eq '.' }
20              
21 15     15   71 use File::Spec ();
  15         21  
  15         351  
22 15     15   25 BEGIN { eval { require Cwd } }
  15         289  
23 15     15   41 use Carp qw(croak);
  15         26  
  15         919  
24 15     15   56 use vars qw($VERSION);
  15         24  
  15         61288  
25             $VERSION = '0.06';
26             $VERSION = $VERSION;
27              
28             require BATsh::Env;
29             require BATsh::CMD;
30             require BATsh::SH;
31              
32             ###############################################################################
33             # Architecture
34             ###############################################################################
35             #
36             # BATsh is a bilingual shell interpreter.
37             #
38             # It splits a script into CMD sections and SH sections, then executes
39             # each section using its own pure-Perl interpreter:
40             #
41             # BATsh::CMD -- cmd.exe command set (SET, ECHO, IF, FOR, GOTO, ...)
42             # BATsh::SH -- sh/bash command set (echo, export, if/fi, for/done, ...)
43             # BATsh::Env -- shared variable store (bridge between both modes)
44             #
45             # MODE DETECTION: first non-empty, non-comment token of each section.
46             # CMD: token is [A-Z 0-9 _ - \ / : . @ %]+ with at least one A-Z
47             # SH: anything else
48             #
49             # SECTION BOUNDARY:
50             # CMD: parenthesis ( ) depth returns to 0
51             # SH: keyword depth (if/fi, for/done, ...) returns to 0
52             #
53             # ENV BRIDGE:
54             # BATsh::Env::STORE is the single variable table.
55             # CMD %VAR% and SH $VAR both read/write the same store.
56             #
57             ###############################################################################
58              
59             ###############################################################################
60             # Global state
61             ###############################################################################
62             my $_TMPCOUNT = 0;
63              
64             # Subroutine registry: { LABEL => \@lines }
65             my %_SUBROUTINES = ();
66              
67             ###############################################################################
68             # Constructor
69             ###############################################################################
70             sub new {
71 0     0 0 0 my ($class, %args) = @_;
72 0         0 BATsh::Env::init();
73 0   0     0 return bless { verbose => $args{verbose} || 0 }, $class;
74             }
75              
76             ###############################################################################
77             # Public run interface
78             ###############################################################################
79             sub run {
80 21     21 0 7244 my ($class_or_self, $file, %args) = @_;
81 21 50       254 unless (-f $file) { croak "BATsh->run: file not found: $file" }
  0         0  
82 21         48 local *SRCFH;
83 21 50       411 open(SRCFH, $file) or croak "BATsh->run: cannot open $file: $!";
84 21         392 my @lines = ;
85 21         150 close(SRCFH);
86 21         45 _ensure_env_init();
87             # Set batch positional parameters: %0 = script path, %1..%9 = args, %* = all args
88 21 50       65 my @script_args = defined($args{args}) ? @{$args{args}} : ();
  21         41  
89 21         72 _set_batch_args($file, @script_args);
90 21         54 _process_lines(@lines);
91 21 50       83 BATsh::SH::fire_exit_trap("BATsh::SH") if defined &BATsh::SH::fire_exit_trap;
92 21         109 return 1;
93             }
94              
95             sub run_string {
96 145     145 0 59421 my ($class_or_self, $source) = @_;
97 145 50       409 croak "BATsh->run_string: source required" unless defined $source;
98 145         534 my @lines = map { "$_\n" } split(/\n/, $source, -1);
  345         887  
99 145         469 _ensure_env_init();
100 145         441 _process_lines(@lines);
101 145 50       1119 BATsh::SH::fire_exit_trap("BATsh::SH") if defined &BATsh::SH::fire_exit_trap;
102 145         973 return 1;
103             }
104              
105             sub run_lines {
106 0     0 0 0 my ($class_or_self, @lines) = @_;
107 0         0 _ensure_env_init();
108 0         0 _process_lines(@lines);
109 0 0       0 BATsh::SH::fire_exit_trap("BATsh::SH") if defined &BATsh::SH::fire_exit_trap;
110 0         0 return 1;
111             }
112              
113             sub _ensure_env_init {
114             # Init only once per process
115 166 50   166   464 BATsh::Env::init() unless %BATsh::Env::STORE;
116             }
117              
118             ###############################################################################
119             # _set_batch_args -- populate %0..%9 and %* in the Env store
120             # %0 = script path (as passed to run())
121             # %1 = first argument, ..., %9 = ninth argument
122             # %* = all arguments joined by single space (does not include %0)
123             ###############################################################################
124             sub _set_batch_args {
125 25     25   100 my ($script, @args) = @_;
126             # Normalise $0: resolve to absolute path using File::Spec
127 25 50       49 my $abs_script = defined $script ? $script : '';
128 25 100 66     349 if ($abs_script ne '' && !File::Spec->file_name_is_absolute($abs_script)) {
129 3 50       11304 my $cwd = defined(&Cwd::cwd) ? Cwd::cwd() : '.';
130 3         102 $abs_script = File::Spec->catfile($cwd, $abs_script);
131             }
132 25         166 BATsh::Env->set('%0', $abs_script);
133 25         63 for my $n (1 .. 9) {
134 225 100       481 BATsh::Env->set("%$n", defined($args[$n - 1]) ? $args[$n - 1] : '');
135             }
136 25         77 BATsh::Env->set('%*', join(' ', @args));
137             }
138              
139             ###############################################################################
140             # classify_token
141             ###############################################################################
142             sub classify_token {
143 484     484 0 796 my ($class_or_token, $token) = @_;
144 484 50       788 unless (defined $token) { $token = $class_or_token }
  484         524  
145 484 100 100     1545 if ($token =~ /\A[A-Z0-9_\-\\\/\.:@%]+\z/ && $token =~ /[A-Z]/) {
146 252         708 return 'CMD';
147             }
148 232         692 return 'SH';
149             }
150              
151             ###############################################################################
152             # Line parser
153             # Returns ($mode, $stripped_line, $first_token)
154             ###############################################################################
155             sub _parse_line {
156 493     493   711 my ($line) = @_;
157 493         784 (my $s = $line) =~ s/\r?\n\z//;
158 493 100       1279 return ('EMPTY', $s, '') if $s =~ /\A\s*\z/;
159 475 100       1431 return ('COMMENT', $s, '') if $s =~ /\A\s*(?:::|\@?REM(?:\s|\z))/i;
160 470 100       792 return ('COMMENT', $s, '') if $s =~ /\A\s*#(?!!)/;
161 469         801 (my $t = $s) =~ s/\A\s+//;
162 469 50       1626 my $first = ($t =~ /\A(\S+)/) ? $1 : '';
163 469         694 return (classify_token($first), $s, $first);
164             }
165              
166             ###############################################################################
167             # CMD section depth: count unquoted ( )
168             ###############################################################################
169             sub _cmd_paren_delta {
170 247     247   326 my ($line) = @_;
171 247         285 my ($delta, $in_q) = (0, 0);
172 247         815 for my $ch (split //, $line) {
173 3838 100       4888 if ($ch eq '"') { $in_q = !$in_q }
  56 100       78  
174             elsif (!$in_q) {
175 3739 100       3969 $delta++ if $ch eq '(';
176 3739 100       4445 $delta-- if $ch eq ')';
177             }
178             }
179 247         531 return $delta;
180             }
181              
182             ###############################################################################
183             # SH section depth
184             ###############################################################################
185             my %_SH_OPEN = map { $_ => 1 } qw(if for while until case function select);
186             my %_SH_CLOSE = map { $_ => 1 } qw(fi done esac);
187              
188             # Net SH block-depth change for an ENTIRE line.
189             #
190             # A single physical line may both open and close a block, e.g.
191             # for i in 1 2 3; do echo $i; done (net 0)
192             # while COND; do ...; done; done (net -1, nested close)
193             # Looking only at the first token (as earlier versions did) counted the
194             # leading "for"/"while" but never the inline "done", so the SH section was
195             # left open and a following CMD line (e.g. ECHO ...%VAR%...) was wrongly
196             # absorbed into the SH section and run by the SH interpreter.
197             #
198             # Opener keywords (if for while until case function select) and "{" add 1;
199             # closer keywords (fi done esac) and "}" subtract 1, but only when they are in
200             # command position (line start, or just after ; & | ( ) { } or do/then/else/
201             # elif). Single- and double-quoted text, $( ... ) command substitutions and
202             # `...` backticks are skipped so that a "done" inside a string or substitution
203             # does not affect the depth. Perl 5.005_03 compatible (no regex features
204             # beyond character classes; substr/index style scanning).
205             sub _sh_depth_delta {
206 230     230   355 my ($line) = @_;
207 230 50       350 return 0 unless defined $line;
208              
209             # Fast path: a bare single keyword token (the common multi-line case).
210 230 100       694 if ($line !~ /[\s;&|(){}'"`]/) {
211 48         94 my $l = lc($line);
212 48 100       130 return 1 if exists $_SH_OPEN{$l};
213 45 100       125 return -1 if exists $_SH_CLOSE{$l};
214 29         70 return 0;
215             }
216              
217 182         201 my $delta = 0;
218 182         188 my $cmdpos = 1; # next bareword starts a new statement?
219 182         194 my $i = 0;
220 182         244 my $n = length($line);
221 182         263 while ($i < $n) {
222 1303         1560 my $c = substr($line, $i, 1);
223              
224 1303 100       1672 if ($c eq "'") { # single-quoted region
225 16         16 $i++;
226 16   66     187 $i++ while $i < $n && substr($line, $i, 1) ne "'";
227 16         14 $i++; $cmdpos = 0; next;
  16         22  
  16         35  
228             }
229 1287 100       1587 if ($c eq '"') { # double-quoted region
230 39         55 $i++;
231 39   66     433 while ($i < $n && substr($line, $i, 1) ne '"') {
232 252 50       394 if (substr($line, $i, 1) eq '\\') { $i += 2; next }
  0         0  
  0         0  
233 252         638 $i++;
234             }
235 39         107 $i++; $cmdpos = 0; next;
  39         50  
  39         63  
236             }
237 1248 100       1508 if ($c eq '`') { # backtick substitution
238 1         3 $i++;
239 1   66     33 $i++ while $i < $n && substr($line, $i, 1) ne '`';
240 1         2 $i++; $cmdpos = 0; next;
  1         2  
  1         4  
241             }
242 1247 100 66     1735 if ($c eq '$' && $i + 1 < $n && substr($line, $i + 1, 1) eq '(') {
      100        
243 6         9 $i += 2; # $( ... ) -- skip balanced parens
244 6         7 my $d = 1;
245 6   100     27 while ($i < $n && $d > 0) {
246 145         146 my $ch = substr($line, $i, 1);
247 145 100       173 $d++ if $ch eq '(';
248 145 100       209 $d-- if $ch eq ')';
249 145         252 $i++;
250             }
251 6         8 $cmdpos = 0; next;
  6         12  
252             }
253 1241 100       1914 if ($c =~ /\s/) { $i++; next }
  533         468  
  533         716  
254 708 100 100     2124 if ($c eq ';' || $c eq '&' || $c eq '|') { $cmdpos = 1; $i++; next }
  75   100     99  
  75         74  
  75         103  
255 633 100       835 if ($c eq '(') { $cmdpos = 1; $i++; next }
  28         33  
  28         25  
  28         42  
256 605 100       803 if ($c eq ')') { $cmdpos = 1; $i++; next }
  31         43  
  31         26  
  31         106  
257 574 100       686 if ($c eq '{') { $delta++ if $cmdpos; $cmdpos = 1; $i++; next }
  9 100       17  
  9         11  
  9         16  
  9         25  
258 565 100       712 if ($c eq '}') { $delta-- if $cmdpos; $cmdpos = 1; $i++; next }
  9 100       17  
  9         16  
  9         13  
  9         14  
259              
260             # Bareword
261 556         602 my $word = '';
262 556         671 while ($i < $n) {
263 2849         2805 my $wc = substr($line, $i, 1);
264 2849 100       4208 last if $wc =~ /[\s;&|(){}'"`]/;
265 2423         2239 $word .= $wc; $i++;
  2423         2746  
266             }
267 556 100       732 if ($cmdpos) {
268 270         481 my $lw = lc($word);
269 270 100       648 if (exists $_SH_OPEN{$lw}) { $delta++ }
  24 100       44  
270 11         14 elsif (exists $_SH_CLOSE{$lw}) { $delta-- }
271             # do/then/else/elif keep the FOLLOWING word in command position so
272             # that an opener directly after them (e.g. "do for ...") is counted.
273 270 100 100     1454 $cmdpos = ($lw eq 'do' || $lw eq 'then'
274             || $lw eq 'else' || $lw eq 'elif') ? 1 : 0;
275             }
276             else {
277 286         377 $cmdpos = 0;
278             }
279             }
280 182         378 return $delta;
281             }
282              
283             ###############################################################################
284             # Subroutine extraction
285             ###############################################################################
286             sub _extract_subroutines {
287 195     195   440 my (@lines) = @_;
288 195         281 my @out = (); my $in_sub = ''; my @sub_body = ();
  195         324  
  195         221  
289              
290             # Determine which :LABEL lines are subroutine ENTRY points (to be lifted
291             # out of the main stream and stored in %_SUBROUTINES) versus ordinary
292             # GOTO labels (which stay in the stream for the CMD interpreter).
293             #
294             # Two independent signals are unioned:
295             #
296             # (a) CALL targets: any label named by a "CALL :LABEL" anywhere in the
297             # script is an entry point. This is the decisive signal -- a label
298             # you CALL is a subroutine -- and it lets a subroutine contain its
299             # own internal GOTO labels without being mis-split.
300             #
301             # (b) The RET heuristic: a label whose block ends in RET/RETURN before
302             # the next label. Kept for backward compatibility so a subroutine
303             # invoked only indirectly is still recognised.
304             #
305             # A label is opened as a subroutine ONLY at the top level (not while
306             # already inside a subroutine body); once inside a body, every :LABEL is
307             # an internal label that travels with the body so that GOTO within the
308             # subroutine resolves, and only RET/RETURN closes the body.
309 195         294 my %is_sub_label = ();
310              
311             # (a) CALL targets (matched anywhere on the line, e.g. "IF .. CALL :X").
312 195         333 for my $line (@lines) {
313 637         2659 (my $s = $line) =~ s/\r?\n\z//;
314 637         1661 while ($s =~ /\bCALL\s+:([A-Za-z_][A-Za-z0-9_]*)/ig) {
315 30         113 $is_sub_label{uc($1)} = 1;
316             }
317             }
318              
319             # (b) RET heuristic.
320             {
321 195         244 my $cur = '';
  195         287  
322 195         235 for my $line (@lines) {
323 637         1635 (my $s = $line) =~ s/\r?\n\z//;
324 637         960 $s =~ s/\A\s+//;
325 637 100 100     2344 if ($s =~ /\A:([A-Za-z_][A-Za-z0-9_]*)\s*\z/) {
    100 66        
    50          
326 63         115 $cur = uc($1);
327             }
328             elsif ($cur ne '' && $s =~ /\A(?:RET|RETURN)\s*\z/i) {
329 25         42 $is_sub_label{$cur} = 1;
330 25         37 $cur = '';
331             }
332             elsif ($cur ne '' && $s =~ /\A:([A-Za-z_][A-Za-z0-9_]*)\s*\z/) {
333             # New label before RET: previous one is a GOTO label, not sub
334 0         0 $cur = uc($1);
335             }
336             }
337             }
338              
339 195         231 for my $line (@lines) {
340 637         1566 (my $s = $line) =~ s/\r?\n\z//;
341 637         833 $s =~ s/\A\s+//;
342 637 100       992 if ($s =~ /\A:([A-Za-z_][A-Za-z0-9_]*)\s*\z/) {
343 63         87 my $lbl = uc($1);
344 63 100       106 if ($in_sub ne '') {
345             # Inside a subroutine: this is an internal label of the body
346             # (a GOTO target), not a new subroutine. Keep it with the
347             # body so the CMD interpreter can resolve GOTO :internal.
348 15         17 push @sub_body, $line;
349 15         18 next;
350             }
351 48 100       78 if ($is_sub_label{$lbl}) {
352             # Top-level subroutine entry: lift it out of the main stream.
353 25         29 $in_sub = $lbl; @sub_body = ();
  25         25  
354 25         41 next; # remove the entry-label line from the stream
355             }
356             # Top-level GOTO label: keep in stream for the CMD interpreter.
357 23         27 push @out, $line;
358 23         30 next;
359             }
360 574 100       766 if ($in_sub ne '') {
361 86 100       136 if ($s =~ /\A(?:RET|RETURN)\s*\z/i) {
362 25         63 $_SUBROUTINES{$in_sub} = [@sub_body];
363 25         28 $in_sub = ''; @sub_body = ();
  25         32  
364             }
365             else {
366 61         69 push @sub_body, $line;
367             }
368 86         130 next;
369             }
370 488         700 push @out, $line;
371             }
372 195 50       425 $_SUBROUTINES{$in_sub} = [@sub_body] if $in_sub ne '';
373 195         758 return @out;
374             }
375              
376             ###############################################################################
377             # call_sub / source_file
378             ###############################################################################
379             sub call_sub {
380 29     29 0 60 my ($class_or_self, $label, @args) = @_;
381 29         33 $label = uc($label); $label =~ s/^://;
  29         38  
382             croak "BATsh->call_sub: undefined subroutine :$label"
383 29 50       58 unless exists $_SUBROUTINES{$label};
384              
385             # --- Save the caller's positional-parameter frame -----------------
386             # CALL :label is a true subroutine call: the callee gets its own
387             # %0..%9 / %* (and the SH-side BATSH_ARG* mirror), and the caller's
388             # parameters are restored on return. An undef snapshot means the key
389             # was absent and must be removed again on restore.
390 29         148 my @frame_keys = ('%0','%1','%2','%3','%4','%5','%6','%7','%8','%9','%*',
391             'BATSH_ARGC',
392             'BATSH_ARG1','BATSH_ARG2','BATSH_ARG3','BATSH_ARG4',
393             'BATSH_ARG5','BATSH_ARG6','BATSH_ARG7','BATSH_ARG8',
394             'BATSH_ARG9');
395 29         27 my %saved;
396 29         38 for my $k (@frame_keys) {
397 609 100       960 $saved{$k} = exists $BATsh::Env::STORE{$k} ? $BATsh::Env::STORE{$k}
398             : undef;
399             }
400              
401             # --- Install the subroutine's own arguments -----------------------
402             # %0 is the label token (":LABEL"), matching cmd.exe, so that %~..0
403             # modifiers and %0 inside the subroutine refer to the label, not the
404             # outer script. %1..%9 are the call arguments; %* is their join.
405 29         129 BATsh::Env->set('%0', ":$label");
406 29         69 for my $n (1 .. 9) {
407 261 100       481 BATsh::Env->set("%$n", defined($args[$n-1]) ? $args[$n-1] : '');
408             }
409 29         103 BATsh::Env->set('%*', join(' ', @args));
410              
411             # SH-side mirror so a subroutine body written in SH mode still sees
412             # $1..$9 / $@. Keys beyond the supplied argument count are removed so
413             # a shorter call does not inherit the caller's stale BATSH_ARG* values.
414 29         47 $BATsh::Env::STORE{'BATSH_ARGC'} = scalar @args;
415 29         57 for my $n (1 .. 9) {
416 261 100       287 if ($n <= scalar @args) {
417 44         77 $BATsh::Env::STORE{"BATSH_ARG$n"} = $args[$n-1];
418             }
419             else {
420 217         257 delete $BATsh::Env::STORE{"BATSH_ARG$n"};
421             }
422             }
423              
424             # --- Run the body, then always restore the caller frame -----------
425 29         38 my $ok = eval { _process_lines(@{$_SUBROUTINES{$label}}); 1 };
  29         27  
  29         82  
  29         63  
426 29         43 my $err = $@;
427 29         55 for my $k (@frame_keys) {
428 609 100       710 if (defined $saved{$k}) { $BATsh::Env::STORE{$k} = $saved{$k} }
  314         383  
429 295         310 else { delete $BATsh::Env::STORE{$k} }
430             }
431 29 50       45 die $err unless $ok;
432 29         149 return 1;
433             }
434              
435             sub source_file {
436 0     0 0 0 my ($class_or_self, $file) = @_;
437 0 0       0 croak "BATsh->source_file: file not found: $file" unless -f $file;
438 0         0 local *SFHH;
439 0 0       0 open(SFHH, $file) or croak "BATsh->source_file: cannot open $file: $!";
440 0         0 my @src = ;
441 0         0 close(SFHH);
442 0         0 _process_lines(@src);
443 0         0 return 1;
444             }
445              
446             ###############################################################################
447             # SETLOCAL / ENDLOCAL (public API)
448             ###############################################################################
449 0     0 0 0 sub setlocal { BATsh::Env::setlocal() }
450 0     0 0 0 sub endlocal { BATsh::Env::endlocal() }
451              
452             ###############################################################################
453             # _exec_cmd_section -- run CMD lines through BATsh::CMD
454             ###############################################################################
455             sub _exec_cmd_section {
456 101     101   138 my (@lines) = @_;
457             # Handle BATsh-native directives before CMD interpreter
458 101         106 my @batch = ();
459 101         113 for my $line (@lines) {
460 243         283 (my $s = $line) =~ s/\r?\n\z//;
461 243         284 $s =~ s/\A\s+//;
462 243 100       465 if ($s =~ /\ASETLOCAL(?:\s+(.*))?\z/i) {
463 4 50       8 my $opts = defined $1 ? $1 : '';
464             # Pass the whole line through to CMD interpreter so it sees
465             # ENABLEDELAYEDEXPANSION etc; also update Env flags here.
466 4         6 push @batch, $line;
467 4         15 next;
468             }
469 239 100       375 if ($s =~ /\AENDLOCAL\s*\z/i) {
470             # Handled inside CMD.pm / _dispatch
471 4         4 push @batch, $line;
472 4         19 next;
473             }
474             # NOTE: CALL :label and CALL file.batsh are intentionally NOT
475             # intercepted here. They are handled by the CMD interpreter's
476             # _cmd_call (which delegates to call_sub / source_file). Letting
477             # CALL stay inside the batch keeps the whole CMD section in one
478             # exec_block, so a GOTO whose loop body contains a CALL still finds
479             # its label, and a subroutine body may use its own internal labels.
480 235         348 push @batch, $line;
481             }
482 101 50       211 _flush_cmd(\@batch) if @batch;
483             }
484              
485             sub _flush_cmd {
486 101     101   122 my ($lines_ref) = @_;
487 101 50       81 return unless @{$lines_ref};
  101         180  
488 101         312 BATsh::CMD::exec_block('BATsh::CMD', $lines_ref,
489             _batsh => __PACKAGE__,
490             _pushd_stack => [],
491             );
492             }
493              
494             ###############################################################################
495             # _exec_sh_section -- run SH lines through BATsh::SH
496             ###############################################################################
497             sub _exec_sh_section {
498 107     107   237 my (@lines) = @_;
499 107         148 my @batch = ();
500 107         176 for my $line (@lines) {
501 268         399 (my $s = $line) =~ s/\r?\n\z//;
502 268         466 $s =~ s/\A\s+//;
503 268 50       449 if ($s =~ /\A(?:source|\.)\s+(\S+\.batsh)/) {
504 0         0 my $bfile = $1;
505 0 0       0 _flush_sh(\@batch) if @batch; @batch = ();
  0         0  
506 0         0 eval { source_file('', $bfile) };
  0         0  
507 0 0       0 warn $@ if $@;
508 0         0 next;
509             }
510 268         418 push @batch, $line;
511             }
512 107 50       262 _flush_sh(\@batch) if @batch;
513             }
514              
515             sub _flush_sh {
516 107     107   192 my ($lines_ref) = @_;
517 107 50       117 return unless @{$lines_ref};
  107         181  
518 107         429 BATsh::SH::exec_block('BATsh::SH', $lines_ref,
519             _batsh => __PACKAGE__,
520             );
521             }
522              
523             ###############################################################################
524             # _process_lines -- main dispatcher
525             ###############################################################################
526             sub _process_lines {
527 195     195   424 my (@source) = @_;
528 195         629 @source = _extract_subroutines(@source);
529              
530 195         252 my @pending = (); my $cur_mode = ''; my $depth = 0;
  195         280  
  195         234  
531              
532             # Here-document tracking: while a here-document body is being collected,
533             # body lines are appended to the current section verbatim and are NOT
534             # reclassified (so an uppercase-leading body line is not misrouted to CMD).
535             # Activation is deferred by one line so the trigger line itself is
536             # classified normally.
537 195         236 my $hd_delim = undef; # active delimiter (collecting body)
538 195         184 my $hd_dash = 0;
539 195         217 my $pending_hd_delim = undef; # set on the trigger line, activated next iter
540 195         196 my $pending_hd_dash = 0;
541              
542 195         1501 for my $raw (@source) {
543 511         748 chomp $raw;
544              
545             # Promote a pending here-document to active (trigger line already pushed)
546 511 100       781 if (defined $pending_hd_delim) {
547 13         47 $hd_delim = $pending_hd_delim;
548 13         24 $hd_dash = $pending_hd_dash;
549 13         21 $pending_hd_delim = undef;
550             }
551              
552             # Collecting a here-document body: pass through unclassified.
553 511 100       743 if (defined $hd_delim) {
554 29         52 push @pending, $raw;
555 29         45 my $probe = $raw;
556 29 100       69 $probe =~ s/\A\t+// if $hd_dash;
557 29 100       55 $hd_delim = undef if $probe eq $hd_delim;
558 29         53 next;
559             }
560              
561 482         803 my ($mode, $line, $first) = _parse_line($raw);
562              
563             # Detect a here-document opener on an SH line; defer activation so the
564             # trigger line is classified normally this iteration.
565 482 100       935 if ($mode eq 'SH') {
566 223         641 my @hd = BATsh::SH::_hd_detect($raw);
567 223 100       397 if (@hd) {
568 13         22 $pending_hd_delim = $hd[2];
569 13         27 $pending_hd_dash = $hd[1];
570             }
571             }
572              
573 482 100 100     1288 if ($mode eq 'EMPTY' || $mode eq 'COMMENT') {
574             # A comment carries no execution effect, so it must never be handed
575             # to an interpreter verbatim. A CMD-style comment (":: ..." or
576             # "REM ...") carried into an SH section would be dispatched to the
577             # external shell (SH treats only "#" as a comment), and any "( )" or
578             # other shell metacharacter in the comment then makes /bin/sh fail
579             # with a syntax error. The reverse ("# ..." into CMD) is likewise
580             # not a CMD comment. Routing it as a BLANK line is skipped
581             # identically by both interpreters and in every nesting depth, and
582             # avoids the real cmd.exe quirk of "::" inside a "( )" block.
583 18 50       45 push @pending, '' if $cur_mode ne '';
584 18         28 next;
585             }
586              
587 464 100       783 if ($cur_mode eq '') {
    100          
588 195         216 $cur_mode = $mode; $depth = 0;
  195         230  
589 195         295 push @pending, $line;
590 195 100       525 $depth += ($mode eq 'CMD') ? _cmd_paren_delta($line) : _sh_depth_delta($line);
591             }
592             elsif ($mode eq $cur_mode) {
593 254         309 push @pending, $line;
594 254 100       415 $depth += ($mode eq 'CMD') ? _cmd_paren_delta($line) : _sh_depth_delta($line);
595 254 50       474 $depth = 0 if $depth < 0;
596             }
597             else {
598 15 100       21 if ($depth > 0) {
599 2         3 push @pending, $line;
600 2 50       11 $depth += ($cur_mode eq 'CMD') ? _cmd_paren_delta($line) : _sh_depth_delta($line);
601 2 50       6 $depth = 0 if $depth < 0;
602             }
603             else {
604 13 50       32 _flush_section($cur_mode, @pending) if @pending;
605 13         31 @pending = ($line); $cur_mode = $mode; $depth = 0;
  13         15  
  13         12  
606 13 100       30 $depth += ($mode eq 'CMD') ? _cmd_paren_delta($line) : _sh_depth_delta($line);
607             }
608             }
609             }
610 195 50       544 _flush_section($cur_mode, @pending) if @pending;
611             }
612              
613             sub _flush_section {
614 208     208   466 my ($mode, @lines) = @_;
615 208 50       341 return unless @lines;
616 208 100       298 if ($mode eq 'CMD') { _exec_cmd_section(@lines) }
  101         161  
617 107         201 else { _exec_sh_section(@lines) }
618             }
619              
620             ###############################################################################
621             # REPL
622             ###############################################################################
623             sub repl {
624 0     0 0 0 my ($class_or_self) = @_;
625 0         0 _ensure_env_init();
626 0         0 print "BATsh $VERSION - Bilingual Shell\n";
627 0         0 print "Uppercase => CMD mode, lowercase => SH mode. EXIT/exit to quit.\n\n";
628              
629 0         0 my (@buf, $depth, $cur_mode) = ((), 0, '');
630 0         0 my ($hd_delim, $hd_dash, $pending_hd_delim, $pending_hd_dash)
631             = (undef, 0, undef, 0);
632 0         0 while (1) {
633 0 0 0     0 print $depth > 0 || defined($hd_delim) ? ' +> ' : 'BATsh> ';
634 0         0 my $line = ;
635 0 0       0 last unless defined $line;
636 0         0 chomp $line;
637              
638             # Promote pending here-document (trigger already buffered)
639 0 0       0 if (defined $pending_hd_delim) {
640 0         0 $hd_delim = $pending_hd_delim; $hd_dash = $pending_hd_dash;
  0         0  
641 0         0 $pending_hd_delim = undef;
642             }
643             # Collecting here-document body: buffer verbatim, do not flush/classify
644 0 0       0 if (defined $hd_delim) {
645 0         0 push @buf, $line;
646 0         0 my $probe = $line;
647 0 0       0 $probe =~ s/\A\t+// if $hd_dash;
648 0 0       0 $hd_delim = undef if $probe eq $hd_delim;
649 0 0 0     0 if (!defined($hd_delim) && $depth == 0) {
650 0         0 _flush_section($cur_mode, @buf);
651 0         0 @buf = (); $cur_mode = ''; $depth = 0;
  0         0  
  0         0  
652             }
653 0         0 next;
654             }
655              
656 0 0       0 if ($line =~ /\A\s*(?:EXIT|exit)\s*\z/) { print "Bye.\n"; last }
  0         0  
  0         0  
657 0 0 0     0 next if $depth == 0 && $line =~ /\A\s*\z/;
658 0         0 push @buf, $line;
659 0         0 my (undef, undef, $first) = _parse_line($line);
660 0 0 0     0 $cur_mode = classify_token($first) if $depth == 0 && $cur_mode eq '';
661             # Detect here-document opener (SH only); defer activation one line
662 0 0       0 if ($cur_mode eq 'SH') {
663 0         0 my @hd = BATsh::SH::_hd_detect($line);
664 0 0       0 if (@hd) { $pending_hd_delim = $hd[2]; $pending_hd_dash = $hd[1] }
  0         0  
  0         0  
665             }
666 0 0       0 $depth += ($cur_mode eq 'CMD') ? _cmd_paren_delta($line) : _sh_depth_delta($line);
667 0 0       0 $depth = 0 if $depth < 0;
668 0 0 0     0 if ($depth == 0 && !defined($pending_hd_delim)) {
669 0         0 _flush_section($cur_mode, @buf);
670 0         0 @buf = (); $cur_mode = ''; $depth = 0;
  0         0  
  0         0  
671             }
672             }
673             }
674              
675             ###############################################################################
676             # Accessors
677             ###############################################################################
678 0     0 0 0 sub version { return $VERSION }
679 2     2 0 42 sub sh_available { return 1 } # always: built-in SH interpreter
680              
681             ###############################################################################
682             # Run as script
683             ###############################################################################
684             unless (caller) {
685             BATsh::Env::init();
686             if (@ARGV == 0) { BATsh->repl() }
687             elsif ($ARGV[0] eq '-e') { shift @ARGV; BATsh->run_string(join("\n", @ARGV)) }
688             else { BATsh->run($ARGV[0]) }
689             }
690              
691             1;
692              
693             __END__