File Coverage

blib/lib/Batch/Interpreter.pm
Criterion Covered Total %
statement 45 1137 3.9
branch 0 726 0.0
condition 0 264 0.0
subroutine 16 108 14.8
pod 10 82 12.2
total 71 2317 3.0


line stmt bran cond sub pod time code
1             package Batch::Interpreter;
2            
3 2     2   57087 use v5.10;
  2         10  
4 2     2   15 use warnings;
  2         5  
  2         70  
5 2     2   12 use strict;
  2         10  
  2         161  
6            
7             =head1 NAME
8            
9             Batch::Interpreter - interpreter for CMD.EXE batch files
10            
11             =head1 VERSION
12            
13             Version 0.01
14            
15             =cut
16            
17             our $VERSION = '0.01';
18            
19            
20             =head1 SYNOPSIS
21             use Batch::Interpreter;
22            
23             open my $fh, '<:crlf', $ARGV[0]
24             or die "$ARGV[0]: $!";
25            
26             my $rc = Batch::Interpreter->new(
27             locale => 'de_DE',
28             # more settings, see below
29             )->run({}, [ <$fh> ], @ARGV);
30            
31             =head1 METHODS
32            
33             =head2 ->new(%settings)
34            
35             Create an instance of the class, which can be custimized with the following parameters (values for keys of C<%settings>):
36            
37             =head3 Customization and Behavior
38            
39             =over
40            
41             =item filenames => $self->default_filenames
42            
43             A Hash with the mapping of emulated filenames to host filenames, the default likely contains the entry NUL => '/dev/null'.
44            
45             =item mountpoints => []
46            
47             Mapping of emulated mount points (in most cases: drives) to host directories. The entries are scanned in order. Example: 'D:\' => '/mnt/data'.
48            
49             =item remote_names => []
50            
51             Mapping of emulated directories to remote names, for prompt $M.
52            
53             =item extension_handlers => $self->builtin_extension_handlers
54            
55             The list of handlers for file extensions. See chapter 'Extension Handlers' for the call interface.
56            
57             =item internal_commands => $self->builtin_internal_commands
58            
59             The list of handlers for internal commands, i.e. commands handled by CMD.EXE. See chapter 'Command Handlers' for the call interface.
60            
61             =item external_commands => $self->builtin_external_commands
62            
63             The list of handlers for external commands, i.e. command line tools. See chapter 'Command Handlers' for the call interface.
64            
65             =item locale
66            
67             The locale, see chapter LOCALES.
68            
69             =back
70            
71            
72             =head3 Interpreter State
73            
74             =over
75            
76             =item echo => 1
77            
78             State of the ECHO setting, 0 = OFF, 1 = ON.
79            
80             =item exit_value => 0
81            
82             %ERRORLEVEL%.
83            
84             =item extensions => 1
85            
86             State of SETLOCAL ENABLEEXTENSIONS / DISABLEEXTENSIONS, 0 = DISABLE, 1 = ENABLE.
87             .
88            
89             Extensions are always enabled, but the state variable is maintained correctly.
90            
91             =item delayedexpansion => 1
92            
93             State of SETLOCAL ENABLEDELAYEDEXPANSION / DISABLEDELAYEDEXPANSION, 0 = DISABLE, 1 = ENABLE.
94            
95             =item vars => {}
96            
97             The values of the environment variables. Due to the case insensitivity all keys are in CAPS, while the real case of the variable names is stored in varcases under the same key.
98            
99             =item varcases => { map((uc $_ => $_), keys %{$self->{vars}}), %{$self->default_variable_cases} }
100            
101             See vars.
102            
103             =back
104            
105             =head3 Defaults and Constants
106            
107             =over
108            
109             =item version_string => __PACKAGE__." $VERSION"
110            
111             The string for prompt $V and the ver command.
112            
113             =item default_drive => 'C'
114            
115             The default drive. Per default a mountpoint for this drive is generated.
116            
117             =item default_pathext => '.COM;.EXE;.BAT'
118            
119             The default, if %PATHEXT% is unset.
120            
121             =back
122            
123             =head3 User Interface
124            
125             =over
126            
127             =item terminal => undef
128            
129             An optional instance of Term::ReadLine or any other class that supports the ->readline($prompt) method.
130            
131             =back
132            
133             =head3 Tracing and Verbosity
134            
135             =over
136            
137             =item dump_parse_tree => undef
138            
139             Dump parse tree of parsed command before execution.
140            
141             =item verbose_system => undef
142            
143             Print command lines before calling system() for external commands.
144            
145             =back
146            
147            
148             =head2 ->run($attr, $lines, @arg)
149            
150             Run the interpreter.
151             If given, C<$lines> is an ArrayRef of the lines of the script, else the commands are read from the terminal (as set in the contructor). The arguments of the script can be passed in C<@arg>. Per default the arguments have to be unquoted, like in perl's @ARGV -- in this case a realistically looking quoting is added internally.
152            
153             C<$attr> has the keys
154            
155             =over
156            
157             =item quoted_arguments => undef
158            
159             Set to true, if C<@arg> contains quoted arguments. In this case the values in C<@arg> have to be quoted like under the emulated system. Specifically, they have to look like the return value of the internal function C, i.e. trailing blanks are mandatory for all but the last argument. In this case the arguments can be used as-is.
160            
161             =back
162            
163            
164             =head1 CUSTOMIZATION
165            
166             =head2 Extension Handlers
167            
168             Extension handlers are CodeRefs, that are called with the arguments:
169            
170             my $ret = $handler->($self, $command, \@arg, $qcommand, \@qarg);
171            
172             C<$self> is the interpreter instance, C<$command> is the name of the command (i.e. the key the handler was found under in the handler hash), C<@arg> is the array of unquoted command line arguments. $C<$qcommand> and C<@qarg> and C<$args> are the raw forms of C<$command> and C<@arg>, i.e. the quoted command and arguments, which are most likely not needed in the handler. C<$command> is resolved via the C<%PATH%>, while C<$qcommand> is not.
173            
174             The code has to return an empty list (to do nothing) or a list with the name of the interpreter and the arguments that lead to the interpretation of C<$command> (likely including C<$command> itself), which is prepended to C<@arg> and C<@qarg> in the calling code.
175            
176             =head2 Command Handlers
177            
178             Command handlers are CodeRefs, that are called with the arguments:
179            
180             my $ret = $handler->($self, $command, \@arg, \@qarg, $args);
181            
182             C<$self> is the interpreter instance, C<$command> is the name of the command (i.e. the key the handler was found under in the handler hash), C<@arg> is the array of unquoted command line arguments. C<@qarg> and C<$args> are the raw forms of C<@arg>, namely the quoted arguments and the unsplit command line, which are most likely not needed in the handler.
183            
184             The handler can:
185            
186             =over
187            
188             =item
189             Execute the command and return a numeric exit code or the value 'keep' to keep the %ERRORLEVEL% unchanged, or return a string beginning with the word 'error' to signal an error.
190            
191             If the handler culminates in calling an external program, it will likely be based on the methods $self->unc2sys($path), $self->unc2sys_arg($path) and $self->run_external_command($attr, $exe, @arg), which are documented elsewhere in this document.
192            
193             =item
194             Optionally change the contents of C<@arg> (and C<@qarg>, if finished is not set) and return a HashRef with the following keys:
195            
196             =over
197            
198             =item type
199            
200             Must be 'rewrite'.
201            
202             =item command
203            
204             Return a new C<$command> to restart the handler search.
205            
206             =back
207            
208             =back
209            
210             =head2 Locales
211            
212             The locale class has to implement the following methods:
213            
214             =head3 ->format_date($year, $month, $day)
215            
216             Format a date for %DATE%, prompt $D, and the date command.
217            
218             =head3 ->format_time_short($hour, $min)
219            
220             Format a time for the time command.
221            
222             =head3 ->format_time($hour, $min, $sec, $sec100)
223            
224             Format a time for %TIME%, and prompt $T.
225            
226             =head3 ->format_file_timedate($year, $month, $day, $hour, $min)
227            
228             Format a file timestamp for the dir command.
229            
230             =head3 ->format_file_timedate_for($year, $month, $day, $hour, $min)
231            
232             Format a file timestamp for for %I in (...) do echo %~tI
233            
234             =head3 ->get_string($category, $key)
235            
236             Get a localized version of string C<$key> in category C<$category>. The only implemented category is 'message'.
237            
238             =head1 METHODS TO BE CALLED FROM COMMAND HANDLERS
239            
240             =cut
241            
242            
243 2     2   1091 use Data::Dump qw(dump);
  2         14097  
  2         191  
244 2     2   24 use Cwd;
  2         7  
  2         131  
245 2     2   1335 use Time::HiRes qw(gettimeofday);
  2         2779  
  2         13  
246 2     2   1667 use List::MoreUtils qw(first_value);
  2         18841  
  2         29  
247 2     2   3115 use File::DosGlob qw(glob);
  2         13246  
  2         16  
248 2     2   120 use File::Spec;
  2         5  
  2         45  
249 2     2   1177 use File::Spec::Win32;
  2         3687  
  2         74  
250 2     2   1411 use File::Temp;
  2         78007  
  2         270  
251 2     2   25 use File::Find;
  2         8  
  2         149  
252 2     2   1302 use File::Copy;
  2         12756  
  2         165  
253 2     2   1079 use Clone qw(clone);
  2         5325  
  2         140  
254 2     2   1136 use POSIX ":sys_wait_h";
  2         11986  
  2         12  
255            
256             my $is_win_host = $^O =~ /Win32/;
257            
258             # TODO: is there a need to port this to Moo??
259             sub new {
260 0     0 1   my ($class, @arg) = @_;
261 0           my $self = bless {
262             var_path_cache => {},
263            
264             dump_parse_tree => undef,
265             verbose_system => undef,
266            
267             echo => 1,
268             exit_value => 0,
269             extensions => 1, # always active :)
270             delayedexpansion => 1,
271             default_pathext => '.COM;.EXE;.BAT',
272             default_drive => 'C',
273             version_string => __PACKAGE__." $VERSION",
274            
275             @arg
276             }, $class;
277            
278 0   0       $self->{vars} //= {};
279             $self->{varcases} //= {
280 0           map((uc $_ => $_), keys %{$self->{vars}}),
281 0   0       %{$self->default_variable_cases},
  0            
282             };
283 0   0       $self->{filenames} //= $self->default_filenames;
284 0   0       $self->{extension_handlers} //= $self->builtin_extension_handlers;
285 0   0       $self->{internal_commands} //= $self->builtin_internal_commands;
286 0   0       $self->{external_commands} //= $self->builtin_external_commands;
287             # no commands are implemented by the windows shell, but not emulated
288 0   0       $self->{assume_is_executable} //= {};
289            
290             # mapping back and forth (potentially)
291 0   0       $self->{filename_map} = { %{delete($self->{filenames}) // {}} };
  0            
292 0           $self->{filename_invmap} = { reverse %{$self->{filename_map}} };
  0            
293            
294 0   0       $self->{remote_name_map} //= delete($self->{remote_names}) // [];
      0        
295             normalize_map($self->{remote_name_map}, sub {
296 0     0     my ($path) = @_;
297 0           $path =~ y|\\|/|;
298 0           return $path;
299 0           }, undef);
300            
301             $self->{mountpoint_map} = [
302             map {
303 0           $_;
304 0           } @{delete($self->{mountpoints}) // [
305 0   0       $self->{default_drive}.':' => '/',
306             ]}
307             ];
308             # need to write system paths with unix slashes to be compatible with # for_first_match
309             normalize_map($self->{mountpoint_map}, sub {
310 0     0     my ($path) = @_;
311 0           $path =~ y|\\|/|;
312 0           return $path;
313             }, sub {
314 0     0     my ($path) = @_;
315 0           $path = File::Spec->rel2abs($path);
316 0           $path =~ y|\\|/|;
317 0           return $path;
318 0           });
319            
320             defined $self->{locale}
321 0 0         or die "missing locale";
322 0 0         if ('' eq ref(my $locale = $self->{locale})) {
323 0           my $first_err;
324 0           my $locale_class = "Batch::Interpreter::Locale::$locale";
325 0           eval "require $locale_class";
326 0 0         if ($@) {
327 0           $first_err = $@;
328 0           $locale_class = $locale;
329 0           eval "require $locale_class";
330             }
331 0 0         $@ and die "couldn't load locale $locale:\n$first_err\n$@";
332 0           $self->{locale} = $locale_class;
333             }
334 0 0         if (my @missing = grep !$self->{locale}->can($_), qw(
335             format_date format_time_short format_time
336             format_file_timedate format_file_timedate_for
337             get_string
338             )) {
339 0           die "invalid locale, no handler for: ", join ', ', @missing;
340             }
341            
342 0           return $self;
343             }
344            
345             sub deep_clone {
346 0     0 0   my ($self, @modifier) = @_;
347 0           return bless clone({ %$self, @modifier }), ref $self;
348             }
349            
350             sub get_message {
351 0     0 0   my ($self, $message) = @_;
352 0   0       return $self->{locale}->get_string('message', $message) // $message;
353             }
354            
355             sub internal_error {
356 0     0 0   my ($self, $message, @arg) = @_;
357 0           return 'error: '.sprintf $message, @arg;
358             }
359            
360             sub syn_error {
361 0     0 0   my ($self, $message, @arg) = @_;
362 0           return 'error: '.sprintf $self->get_message($message), @arg;
363             }
364            
365             sub os_error {
366 0     0 0   my ($self, $name, $message, @arg) = @_;
367 0   0       return 'error: '.sprintf($self->get_message($message // ''), @arg).
368             " '$name': $!";
369             }
370            
371 2     2   626 eval q{
  0            
  0            
372             use Win32::ShellQuote qw(quote_native);
373             };
374             $@ and eval {
375             sub quote_native {
376 0     0 0   my ($str) = @_;
377 0           $str =~ s/(\W)/\\$1/g;
378 0           return $str;
379             }
380             };
381            
382             sub set_variable {
383 0     0 0   my ($self, $variable, $value) = @_;
384            
385             $self->{verbose_set}
386 0 0         and say STDERR "SET($variable=$value)";
387            
388 0 0 0       defined($value) && '' eq $value
389             and undef $value;
390            
391 0           my $uc_var = uc $variable;
392            
393 0 0 0       if (defined($value)
    0          
394             ? defined $self->{vars}{$uc_var} &&
395             $self->{vars}{$uc_var} eq $value
396             : !exists $self->{vars}{$uc_var}
397             ) {
398             # no value change
399 0           $self->{varcases}{$uc_var} = $variable;
400 0           return;
401             }
402            
403 0 0         if (defined $value) {
404 0           $self->{vars}{$uc_var} = $value;
405 0           $self->{varcases}{$uc_var} = $variable;
406             } else {
407 0           delete $self->{vars}{$uc_var};
408 0           delete $self->{varcases}{$uc_var};
409             }
410            
411 0 0         if ($uc_var eq 'PATHEXT') {
412 0           %{$self->{var_path_cache}} = ();
  0            
413             } else {
414 0           delete $self->{var_path_cache}{$uc_var};
415             }
416             }
417            
418             sub get_date {
419 0     0 0   my ($self) = @_;
420 0           my @localtime = localtime;
421             return $self->{locale}->format_date(
422 0           $localtime[5]+1900, $localtime[4]+1, $localtime[3]
423             );
424             }
425            
426             sub get_time {
427 0     0 0   my ($self, $short) = @_;
428            
429 0           my @time = gettimeofday;
430 0           my @localtime = localtime $time[0];
431 0 0         if ($short) {
432             return $self->{locale}->format_time_short(
433 0           $localtime[2], $localtime[1]
434             );
435             } else {
436             return $self->{locale}->format_time(
437 0           $localtime[2], $localtime[1],
438             $localtime[0], $time[1]/10000
439             );
440             }
441             }
442            
443            
444             sub normalize_map {
445 0     0 0   my ($map, $callback_unc, $callback_sys) = @_;
446 0           for (0..$#$map) {
447 0 0         if (my $callback = ($_ & 1) ? $callback_sys : $callback_unc) {
    0          
448 0           my $path = $callback->($map->[$_]);
449 0           $path =~ s/(?
450 0           $map->[$_] = $path;
451             }
452             }
453             }
454            
455             sub for_first_match {
456 0     0 0   my ($path, $map, $cmp, $callback) = @_;
457            
458 0           (my $mpath = uc $path) =~ y|\\|/|;
459 0           $mpath =~ s/(?
460 0           for my $i (0..($#$map-1)/2) {
461 0 0         if (uc($map->[$i*2+$cmp]) eq
462             substr $mpath, 0, length $map->[$i*2+$cmp]
463             ) {
464 0           $callback->(
465             length $map->[$i*2+$cmp], $map->[$i*2+1-$cmp]
466             );
467 0           last;
468             }
469             }
470             }
471            
472            
473             =head2 ->unc2sys($path)
474            
475             Translates an emulated path into a host path.
476            
477             Convert path separators from \ to / and apply the filename and mountpoint translation rules, return the result.
478            
479             Bug: the path is not actually UNC, though that _may_ work through mountpoint translation.
480            
481             =cut
482             my $switch_slashes = !$is_win_host;
483             sub unc2sys {
484 0     0 1   my ($self, $path) = @_;
485            
486 0 0         $switch_slashes and $path =~ y|\\|/|;
487            
488 0 0         if (exists $self->{filename_map}{uc $path}) {
489 0 0         defined($_ = $self->{filename_map}{uc $path})
490             and return $_;
491             }
492            
493 0 0         if (File::Spec->file_name_is_absolute($path)) {
494             for_first_match $path, $self->{mountpoint_map}, 0, sub {
495 0     0     my ($offs, $val) = @_;
496 0           substr $path, 0, $offs, $val;
497 0           };
498             }
499            
500 0           return $path;
501             }
502            
503             =head2 ->sys2unc($path)
504            
505             Translates a host path into an emulated path.
506            
507             Convert path separators from / to \ and apply the filename and mountpoint translation rules, return the result.
508            
509             Bug: the path is not actually UNC, though that _may_ work through mountpoint translation.
510            
511             =cut
512             sub sys2unc {
513 0     0 1   my ($self, $path) = @_;
514            
515 0 0         if (exists $self->{filename_invmap}{$path}) {
516             # TODO: slashes are not switched...
517 0 0         defined($_ = $self->{filename_invmap}{$path})
518             and return $_;
519             }
520            
521 0 0         if (File::Spec::Win32->file_name_is_absolute($path)) {
522             for_first_match $path, $self->{mountpoint_map}, 1, sub {
523 0     0     my ($offs, $val) = @_;
524 0           substr $path, 0, $offs, $val;
525 0           };
526             }
527            
528             #$switch_slashes and
529 0           $path =~ y|/|\\|;
530            
531 0           return $path;
532             }
533            
534            
535             =head2 ->unc2sys_arg($arg)
536            
537             Translates an emulated command line argument into a host command line argument.
538            
539             Paths are found in the argument by crude heuristics, which are subject to change. The actual translation is done with ->unc2sys($arg).
540            
541             =cut
542             sub unc2sys_arg {
543 0     0 1   my ($self, $arg) = @_;
544            
545 0 0         if (exists $self->{filename_map}{uc $arg}) {
546 0 0         defined($_ = $self->{filename_map}{uc $arg})
547             and return $_;
548             }
549            
550             # try to translate filenames inside the arguments.
551 0           while ($arg =~ /\G.*?(
552             (?
553             (?:
554             # absolute path
555             (?: [A-Z]\: )?
556             (?: \\ [^\\\:]+ )+ \\?
557             |
558             # relative path
559             (?: [^\\\:]+\\ )+ [^\\\:]*
560             )
561             (?! [\w\\\:] )
562             )\b/gcix) {
563 0           my ($pos, $path) = (pos($arg), $1);
564 0           my $newpath = $self->unc2sys($path);
565 0           substr $arg, $pos - length($path), length($path), $newpath;
566 0           pos $arg = $pos - length($path) + length($newpath);
567             }
568 0           return $arg;
569             }
570            
571             sub filter_glob {
572 0     0 0   my ($self, $filter, $do_glob, $uncglob) = @_;
573 0           my $sysglob = $self->unc2sys($uncglob);
574 0           my @list;
575 0 0         if ($filter eq 'directories') {
    0          
576 0           @list = grep -d, glob $sysglob;
577             } elsif ($filter eq 'files') {
578 0           @list = grep -f, glob $sysglob;
579             } else {
580 0 0         if ($do_glob) {
581 0           @list = grep -f, glob $sysglob;
582             } else {
583 0           @list = ($sysglob);
584             }
585 0           @list = map File::Spec->rel2abs($_), @list;
586             }
587 0           return map $self->sys2unc($_), @list;
588             }
589            
590             sub set_curdir {
591 0     0 0   my ($self, $dir) = @_;
592 0           undef $self->{curdir};
593 0           return chdir $self->unc2sys($dir);
594             }
595            
596             sub get_curdir {
597 0     0 0   my ($self) = @_;
598             return $self->{curdir} //=
599 0   0       File::Spec::Win32->catdir($self->sys2unc(getcwd));
600             }
601            
602            
603             sub get_remote_name {
604 0     0 0   my ($self, $path) = @_;
605 0           my $result;
606             for_first_match $path, $self->{remote_name_map}, 0, sub {
607 0     0     my ($offs, $val) = @_;
608 0           $result = $val;
609 0           };
610 0   0       return $result // '';
611             }
612            
613             sub get_drive {
614 0     0 0   my ($self, $path) = @_;
615             return (File::Spec::Win32->splitpath($path))[0]
616 0   0       // $self->{default_drive}.':';
617             }
618            
619             sub get_path {
620 0     0 0   my ($self, $path) = @_;
621 0   0       return (File::Spec::Win32->splitpath($path))[1] // '\\';
622             }
623            
624             sub get_filename {
625 0     0 0   my ($self, $path) = @_;
626 0           my $name = (File::Spec::Win32->splitpath($path))[2];
627 0           $name =~ s/\.[^\.]+$//;
628 0           return $name;
629             }
630            
631             sub get_fileext {
632 0     0 0   my ($self, $path) = @_;
633 0           my $name = (File::Spec::Win32->splitpath($path))[2];
634 0 0         $name =~ /(\.[^\.]+)$/
635             and return $1;
636 0           return '';
637             }
638            
639             sub get_short_path {
640 0     0 0   my ($self, $path) = @_;
641             # TODO: not implemented
642 0           return $path;
643             }
644            
645             sub get_file_attr {
646 0     0 0   my ($self, $syspath) = @_;
647             # TODO: too simple
648 0           return '--a------';
649             }
650            
651             sub get_file_timedate {
652 0     0 0   my ($self, $syspath, $mode) = @_;
653 0   0       my @localtime = localtime((stat $syspath)[9] // 0);
654 0           my @timedate = (
655             $localtime[5]+1900, $localtime[4]+1, $localtime[3],
656             $localtime[2], $localtime[1],
657             );
658             return ($mode//'') eq 'for'
659             ? $self->{locale}->format_file_timedate_for(@timedate)
660 0 0 0       : $self->{locale}->format_file_timedate(@timedate)
661             ;
662             }
663            
664             sub format_size {
665 0     0 0   my ($self, $size) = @_;
666 0           1 while $size =~ s/(?<=\d)(\d\d\d)(?!\d)/.$1/;
667 0           return $size;
668             }
669            
670             sub set_subprocess_env {
671 0     0 0   my ($self, $add_env) = @_;
672            
673 0           my %old = %ENV;
674            
675 0           my $case = $self->{varcases};
676 0           my @copy = grep exists($case->{$_}), keys %{$self->{vars}};
  0            
677            
678             # select the variables that can be safely copied
679 0 0         if ($is_win_host) {
680             # setting a variable converts its case
681             #@copy = grep $_ eq ($case->{$_} // 'a'), @copy;
682             } else {
683             }
684            
685             # TODO: convert the variables that have to be translated
686 0           @ENV{@$case{@copy}} = @{$self->{vars}}{@copy};
  0            
687             $add_env and
688 0 0         @ENV{keys %$add_env} = values %$add_env;
689            
690 0           return %old;
691             }
692            
693             sub extract_for_tokens {
694 0     0 0   my ($opts, $line) = @_;
695            
696 0 0         if ($opts->{skip}) {
697 0           $opts->{skip}--;
698 0           return;
699             }
700            
701             # skip empty lines
702 0 0         $line =~ /\S/ or return;
703            
704 0 0         $opts->{eol_re} and $line =~ s/$opts->{eol_re}/$1/;
705             return map $_ // '', ($opts->{delim_re}
706             ? split($opts->{delim_re}, $line, $opts->{numvals})
707             : ($line)
708 0 0 0       )[@{$opts->{tokens}}];
  0            
709             }
710            
711             my $re_quoted = qr/ \" (?:\\.|\"\"|[^\\\"])*+ (?:\"|$) /x;
712             my $re_quotesc = qr/ \^. | $re_quoted /xo;
713             my $re_string =
714             qr/ (?: $re_quotesc | \d(?!\>) | [^\<\>\|\&\(\)\"\^\s] )++ /xo;
715             my $re_lhs =
716             qr/ (?: $re_quotesc | \d(?!\>) | [^\<\>\|\&\(\)\"\^\s\=] )++ /xo;
717             my $re_call_arg =
718             qr/ (?: $re_quotesc | \d(?!\>) | [^\<\>\|\&\(\)\"\^\s\=\,] )++ /xo;
719             my $re_call_arg_separator = qr/ [\=\,\s] /x;
720             my $re_redirect = qr/ \< | \d?\>\>?(?:\&\d?)? /x;
721             my $re_pipe = qr/ \|\|? | \&\&? /x;
722             my $re_grouping = qr/ [\(\)] /x;
723             my $any_token = qr/$re_redirect | $re_pipe | $re_grouping | $re_string/x;
724            
725             sub next_token {
726             # trailing separator is included to not have to store separators
727             # separately. the separatator will be removed in unquote_token.
728 0 0   0 0   $_[0] =~ /\G ( $any_token \s*+ ) /gcxo
729             or return;
730 0           return $1;
731             }
732            
733             sub next_token_no_pipe {
734 0 0   0 0   $_[0] =~ /\G ( (?:$re_redirect | $re_grouping | $re_string) \s*+ ) /gcxo
735             or return;
736 0           return $1;
737             }
738            
739             sub next_token_string {
740 0 0   0 0   $_[0] =~ /\G ( $re_string \s*+ ) /gcxo
741             or return;
742 0           return $1;
743             }
744            
745             sub next_token_lhs {
746 0 0   0 0   $_[0] =~ /\G ( $re_lhs \s*+ ) /gcxo
747             or return;
748 0           return $1;
749             }
750            
751             sub next_token_call_arg {
752             # semantics different from next_token, because no unquote_token is
753             # following
754 0 0   0 0   $_[0] =~ /\G ( $re_call_arg ) $re_call_arg_separator*+ /gcxo
755             or return;
756 0           return $1;
757             }
758            
759             sub unquote_token {
760 0     0 0   my ($token) = @_;
761 0 0         defined $token or return;
762            
763 0           while ($token =~ /\G [^\"\^\s]* ( $re_quotesc | \s+ ) /gcxo) {
764 0           my ($p, $q) = (pos($token) - length($1), $1);
765 0 0 0       if ($q =~ /^\s+$/ && pos $token == length $token) {
    0          
766             # remove space at end of string
767 0           substr($token, $p) = '';
768 0           last; # end of string
769             } elsif ($q =~ /^\^(.)/) {
770 0           substr $token, $p, 1, '';
771             # reparse escaped " character
772 0 0         pos $token = $p + ($1 eq '"' ? 0 : 1);
773             } else {
774 0 0         my $r = substr $q, 1, $q =~ /\"$/ ? -1 : length $q;
775 0           $r =~ s/(\\[\\\"]|\"\")/substr $1, 1/ge;
  0            
776 0           substr $token, $p, length($q), $r;
777 0           pos $token = $p + length $r;
778             }
779             }
780            
781 0           return $token;
782             }
783            
784             sub requote {
785 0     0 0   my ($arg) = @_;
786             # do the equivalent of Win32::ShellQuote
787            
788             # quote complex arguments
789 0 0         if ($arg =~ /[\s\"]/) {
790 0           $arg =~ s/([\\\"])/\\$1/g;
791 0           $arg = "\"$arg\"";
792             }
793            
794             # add separation blank as in internal calls
795 0           return $arg . ' ';
796             }
797            
798             sub unescape {
799 0     0 0   my ($str) = @_;
800            
801 0           while ($str =~ /\G (?: $re_quoted | [^\"\^] )*+ \^ (.) /gcxo) {
802 0           my $p = pos($str) - 2;
803 0           substr $str, $p, 1, '';
804 0           pos $str = $p + 1;
805             }
806            
807 0           return $str;
808             }
809            
810             # mini-unquote to remove just the trailing separation blanks
811             sub trim_quoted {
812 0 0   0 0   $_[0] =~ /^ ( $any_token (?: \s*+ $any_token )* ) \s* $/xo
813             or return $_[0]; # maybe empty..
814 0           return $1;
815             }
816            
817             sub has_escaped_newline {
818 0 0   0 0   $_[0] =~ /^ ( (?: \s*+ $any_token )* \s*+ ) \^\n $/xo
819             or return;
820 0           return $1;
821             }
822            
823             sub rebase_numbers {
824 0     0 0   my ($str) = @_;
825 0           while ($str =~ /\G\b(0(?:[0-7]++|x[0-9a-f]++|b[01]++))\b/gi) {
826 0           my $num = $1;
827 0           my $start = pos($str) - length $num;
828            
829 0           my $replace = oct $num;
830 0           substr $str, $start, length($num), $replace;
831 0           pos $str = $start + length $replace;
832             }
833 0           return $str;
834             }
835            
836             my %find_marker = (
837             '%' => [ qr/[^\%]*+\%/, qr/([^\%]*+)\%/ ],
838             '!' => [ qr/[^\!]*+\!/, qr/([^\!]*+)\!/ ],
839             );
840             sub replace_env_variables {
841 0     0 0   my ($self, $marker, $str) = @_;
842 0           chomp $str;
843            
844             my ($find_start, $find_end) =
845 0   0       @{$find_marker{$marker} // die "unknown marker '$marker'"};
  0            
846            
847 0           while ($str =~ /\G$find_start/g) {
848 0           my $start = pos($str) - 1;
849 0           my ($replace, $end);
850            
851 0 0         if ($str =~ /\G((?:\~[a-z]*)?)(\d|\*)/gci) {
    0          
852 0           $end = pos $str;
853 0 0         if ($2 eq '*') {
854 0 0         if ($1 eq '') {
855             # orig_args are still quoted, thus
856             # have their trailing blank
857             $replace = trim_quoted join '',
858 0           @{$self->{orig_args}};
  0            
859             } else {
860             # TODO...
861 0           $replace = 'illegal expansion';
862             }
863             } else {
864 0   0       $replace = $self->{args}[$2] // '';
865 0 0         if ($1 eq '') {
    0          
866             # ... but remove the blank here
867 0           $replace = trim_quoted $replace;
868             } elsif ($1 eq '~') {
869 0           $replace = unquote_token $replace;
870             } else {
871             # modifiers kill the value??
872 0           $replace = '';
873             }
874             }
875             } elsif ($str =~ /\G$find_end/gc) {
876 0           my $expr = $1;
877 0           $end = pos $str;
878 0           my $vars = $self->{vars};
879            
880 0           my ($name, $mod) = split /\:/, $expr, 2;
881 0   0       $name //= ''; # if $expr eq ''
882 0 0         my $premod = $name =~ s/^(~)// ? $1 : '';
883            
884 0           my $val;
885            
886 0           my $uc_name = uc $name;
887 0 0 0       if (exists $vars->{$uc_name}) {
    0 0        
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
888 0           $val = $vars->{$uc_name};
889             } elsif ('CD' eq $uc_name) {
890 0           $val = $self->get_curdir;
891             } elsif ('DATE' eq $uc_name) {
892 0           $val = $self->get_date;
893             } elsif ('TIME' eq $uc_name) {
894 0           $val = $self->get_time;
895             } elsif ('RANDOM' eq $uc_name) {
896 0           $val = int rand 32768;
897             } elsif ('ERRORLEVEL' eq $uc_name) {
898 0           $val = $self->{exit_value};
899             } elsif ('CMDEXTVERSION' eq $uc_name) {
900 0           $val = 1;
901             } elsif ('CMDCMDLINE' eq $uc_name) {
902 0           $val = 'C:\\Windows\\system32\\cmd.exe';
903             } elsif ('HIGHESTNUMANODENUMBER' eq $uc_name) {
904 0           $val = 1;
905             } elsif ('' eq $uc_name && !$mod && !$premod) {
906 0 0         $val = $marker
907             if $marker eq '%';
908             }
909            
910 0 0 0       defined $val && $premod eq '~'
911             and $val = unquote_token $val;
912            
913 0 0 0       if (defined $mod && defined $val) {
914 0 0         if ($mod =~ /^(\*?[^=]++)\=(.*+)$/) {
    0          
915 0           my ($from, $to) = ($1, $2);
916 0 0         if ($from =~ s/^\*//) {
917 0           $val =~ s/^.*?\Q$from\E/$to/i;
918             } else {
919 0           $val =~ s/\Q$from\E/$to/ig;
920             }
921             } elsif ($mod =~ /^\~(-?\d++)(?:,(-?\d++))?$/) {
922 0           my ($offs, $len) = ($1, $2);
923             $_ < 0 and $_ .= length $val
924 0   0       for $offs, $len;
925 0 0 0       if ($offs >= 0 && $offs < length $val
      0        
926             && $len >= 0) {
927 0           $val = substr $val, $offs, $len;
928             } else {
929 0           undef $val;
930             }
931             } else {
932 0           undef $val;
933             }
934             }
935            
936 0 0         if (defined $val) {
937 0           $replace = $val;
938             } else {
939 0           $replace = substr $str, $start, $end-$start;
940             }
941             } else {
942 0           last;
943             }
944 0           substr $str, $start, $end-$start, $replace;
945 0           pos $str = $start + length $replace;
946             }
947            
948 0           return $str;
949             }
950            
951             sub find_in_path {
952 0     0 0   my ($self, $var, $filename) = @_;
953            
954 0 0         (File::Spec::Win32->splitpath($filename))[2] eq $filename
955             or return $filename;
956            
957             my @pathext = split ';',
958 0   0       $self->{vars}{PATHEXT} // $self->{default_pathext};
959            
960 0   0       return $self->{var_path_cache}{$var}{$filename} //= do {
961             my $found =
962             first_value {
963             #say "EXIST? ".$_;
964 0     0     -f $self->unc2sys($_)
965             }
966             map {
967 0           my $f = $_;
968 0           ($f, map "$f$_", @pathext);
969             }
970             map File::Spec::Win32->catfile($_, $filename),
971 0   0       split ';', $self->{vars}{$var} // ''
972             ;
973            
974 0 0         defined $found ? $self->sys2unc($found) : undef;
975             };
976             }
977            
978             sub apply_var_modifiers {
979 0     0 0   my ($self, $mod, $replace) = @_;
980            
981 0           my $sysfile = File::Spec->rel2abs($self->unc2sys($replace));
982 0           my $file = $self->sys2unc($sysfile);
983            
984 0 0         -e $sysfile or return $file;
985            
986 0           $mod =~ s/f/dpnx/g;
987 0 0         $mod =~ /s/ and $file = $self->get_short_path($file);
988            
989 0           $replace = '';
990 0 0         $mod =~ /a/ and $replace .= $self->get_file_attr($sysfile);
991 0 0         $mod =~ /t/ and $replace .= ($replace ? ' ' : '').
    0          
992             $self->get_file_timedate($sysfile, 'for');
993 0 0 0       $mod =~ /z/ and $replace .= ($replace ? ' ' : '').(-s $sysfile || '');
    0          
994            
995 0 0 0       $mod =~ /[dpnx]/ && $replace and $replace .= ' ';
996            
997 0 0         $mod =~ /d/ and $replace .= $self->get_drive($file);
998 0 0         $mod =~ /p/ and $replace .= $self->get_path($file);
999 0 0         $mod =~ /n/ and $replace .= $self->get_filename($file);
1000 0 0         $mod =~ /x/ and $replace .= $self->get_fileext($file);
1001            
1002 0           return $replace;
1003             }
1004            
1005             sub replace_for_variables {
1006 0     0 0   my ($self, $str) = @_;
1007            
1008 0           my $for_vars = $self->{for_vars};
1009 0           my $for_keys = join '', grep defined($for_vars->{$_}), keys %$for_vars;
1010 0           my $for_key_pat = qr/[\~\Q$for_keys\E]/;
1011 0           my $after_tilde_pat = qr/([^\Q$for_keys\E]*)([\Q$for_keys\E])/;
1012            
1013 0           pos $str = 0;
1014            
1015 0           while ($str =~ /\G[^\%]*+\%($for_key_pat)/gc) {
1016 0           my $first = $1;
1017 0           my $start = pos($str) - 2;
1018 0           my ($replace, $end);
1019            
1020 0 0         if ($first eq '~') {
1021 0 0 0       if ($str =~ /\G$after_tilde_pat/gc &&
      0        
1022             defined(my $var = $2) &&
1023             $1 =~ /^([fdpnxsatz]*+)(?:\$(\w++)\:)?$/
1024             ) {
1025 0           my ($mod, $path) = ($1, $2);
1026 0           $replace = unquote_token $for_vars->{$var};
1027 0           $end = pos $str;
1028            
1029 0 0 0       defined $path and
1030             $replace = $self->find_in_path(
1031             uc($path), $replace
1032             ) // ''
1033             ;
1034 0 0         $mod and $replace = $self->apply_var_modifiers(
1035             $mod, $replace
1036             );
1037             } else {
1038 0           $end = pos $str;
1039 0           $replace = substr $str, $start, $end-$start;
1040             }
1041             } else {
1042 0           ($replace, $end) = ($for_vars->{$first}, pos $str);
1043             }
1044            
1045 0           substr $str, $start, $end-$start, $replace;
1046 0           pos $str = $start + length $replace;
1047             }
1048            
1049 0           return $str;
1050             }
1051            
1052             sub replace_late_variables {
1053 0     0 0   my ($self, $str) = @_;
1054            
1055 0 0         keys %{$self->{for_vars}}
  0            
1056             and $str = $self->replace_for_variables($str);
1057             $self->{delayedexpansion}
1058 0 0         and $str = $self->replace_env_variables('!', $str);
1059            
1060 0           return $str;
1061             }
1062            
1063             my %prompt_replacement = (
1064             A => '&',
1065             B => '|',
1066             C => '(',
1067             E => "\x1b",
1068             F => ')',
1069             G => '>',
1070             H => "\x08",
1071             L => '<',
1072             Q => '=',
1073             S => ' ',
1074             '_' => "\n",
1075             '$' => '$',
1076             );
1077             sub get_prompt {
1078 0     0 0   my ($self) = @_;
1079 0   0       my $prompt = $self->{vars}{PROMPT} // '$N$G';
1080 0           my $result = '';
1081 0           while ($prompt =~ /\G([^\$]*+)\$(.)/gc) {
1082 0           $result .= $1;
1083 0           my $c = uc $2;
1084 0 0         if (exists $prompt_replacement{$c}) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
1085 0           $result .= $prompt_replacement{$c};
1086             } elsif ($c eq '+') {
1087 0           $result .= '+' x @{$self->{pushd_stack}};
  0            
1088             } elsif ($c eq 'D') {
1089 0           $result .= get_date;
1090             } elsif ($c eq 'M') {
1091 0           $result .= $self->get_remote_name($self->get_curdir);
1092             } elsif ($c eq 'N') {
1093 0           $result .= $self->get_drive($self->get_curdir);
1094             } elsif ($c eq 'P') {
1095 0           $result .= $self->get_curdir;
1096             } elsif ($c eq 'T') {
1097 0           $result .= $self->get_time;
1098             } elsif ($c eq 'V') {
1099 0           $result .= $self->{version_string};
1100             }
1101             }
1102 0 0         $prompt =~ /\G(.+)$/g
1103             and $result .= $1;
1104            
1105 0           return $result;
1106             }
1107            
1108             my %named_fileno = (
1109             STDIN => 0,
1110             STDOUT => 1,
1111             STDERR => 2,
1112             );
1113             sub set_handle {
1114 0     0 0   my ($self, $number, $file) = @_;
1115 0   0       my $fileno = $named_fileno{$number} // $number;
1116            
1117 0           my $old;
1118 0 0         if ($fileno == 0) {
    0          
    0          
1119 0           open $old, '<&', \*STDIN;
1120 0           open STDIN, '<&', $file;
1121             } elsif ($fileno == 1) {
1122 0           open $old, '>&', \*STDOUT;
1123 0           open STDOUT, '>&', $file;
1124             } elsif ($fileno == 2) {
1125 0           open $old, '>&', \*STDERR;
1126 0           open STDERR, '>&', $file;
1127             }
1128            
1129 0   0       $self->{saved_handles}{$fileno} //= $old;
1130             }
1131            
1132             sub print_stdout {
1133 0     0 0   my ($self, @string) = @_;
1134 0 0 0       if (exists $self->{saved_handles} &&
1135             exists $self->{saved_handles}{1}) {
1136 0           my $out = $self->{saved_handles}{1};
1137 0           say $out @string;
1138             } else {
1139 0           say @string;
1140             }
1141             }
1142            
1143             sub reset_handles {
1144 0     0 0   my ($self) = @_;
1145 0 0         if (my $saved = delete $self->{saved_handles}) {
1146 0           my $old;
1147 0 0         if ($old = $saved->{0}) {
1148 0           open STDIN, '<&', $old;
1149             }
1150 0 0         if ($old = $saved->{1}) {
1151 0           open STDOUT, '>&', $old;
1152             }
1153 0 0         if ($old = $saved->{2}) {
1154 0           open STDERR, '>&', $old;
1155             }
1156             }
1157             }
1158            
1159             sub rest {
1160 0     0 0   my ($cur) = @_;
1161 0   0       return substr($$cur, pos($$cur) // 0);
1162             }
1163            
1164             sub trace_statement {
1165 0     0 0   my ($cur, $stop) = @_;
1166            
1167 0   0       my $start = pos($$cur) // 0;
1168 0           my $end;
1169            
1170 0           while (1) {
1171 0           $end = pos $$cur;
1172            
1173 0           my $token = next_token_no_pipe $$cur;
1174 0 0 0       defined $token && $token ne ''
1175             or last;
1176            
1177 0 0         if ($stop) {
1178 0           (my $trim = $token) =~ s/\s*$//;
1179 0 0         $stop->{lc $trim}
1180             and last;
1181             }
1182             }
1183            
1184 0           my $statement;
1185 0 0         $statement = substr $$cur, $start, $end-$start
1186             unless $start == $end;
1187 0           pos $$cur = $end;
1188            
1189 0           return $statement;
1190             }
1191            
1192             sub readline {
1193 0     0 0   my ($self, $prompt) = @_;
1194 0 0         $self->{terminal} or return;
1195 0           return $self->{terminal}->readline($prompt);
1196             }
1197            
1198             sub get_raw_line {
1199 0     0 0   my ($self, $is_continuation) = @_;
1200            
1201 0 0         if ($self->{lines}) {
1202             defined $self->{pos}
1203 0 0         or return;
1204            
1205 0           my $raw_line = $self->{lines}[$self->{pos}++];
1206 0           $self->{pos} > $#{$self->{lines}}
1207 0 0         and undef $self->{pos};
1208            
1209 0           return $raw_line;
1210             } else {
1211 0 0         my $prompt = $is_continuation
    0          
1212             ? $self->get_message('More? ')
1213             : ($is_win_host ? '' : "\n").$self->get_prompt;
1214            
1215 0           return $self->readline($prompt)
1216             }
1217             }
1218            
1219             sub get_next_line {
1220 0     0 0   my ($self, $pline, $is_continuation) = @_;
1221            
1222 0           my $raw_line;
1223             read_raw_line:
1224             {
1225 0 0         if (defined(my $part = $self->get_raw_line($is_continuation))) {
  0            
1226 0 0         if (defined(my $head = has_escaped_newline $part)) {
1227 0   0       $raw_line = ($raw_line // '') . $head;
1228 0           redo read_raw_line;
1229             } else {
1230 0   0       $raw_line = ($raw_line // '') . $part;
1231             }
1232             }
1233             }
1234            
1235 0           $$pline = $self->replace_env_variables('%', $raw_line);
1236            
1237 0           return $pline;
1238             }
1239            
1240             sub parse_cmd;
1241             sub parse_cmd {
1242 0     0 0   my ($self, $cur, $stop) = @_;
1243            
1244 0           my $do_echo = $self->{echo};
1245 0           my $is_start;
1246            
1247 0 0         unless (defined $cur) {
1248 0           $is_start = 1;
1249            
1250 0           $cur = \my $line;
1251            
1252 0 0         $self->get_next_line($cur)
1253             or return 'return';
1254             }
1255            
1256 0           $$cur =~ /\G\s+/gc;
1257 0 0         $$cur =~ /\G\@\s*/gc and $do_echo = 0;
1258 0 0         $$cur =~ /\G\:/gc and return ':';
1259 0 0         $$cur =~ /\G\s*$/gc and return 'empty';
1260 0 0         $$cur =~ /\Grem\b/igc and return 'rem';
1261 0 0         $$cur =~ /\G\)/igc and return ')';
1262            
1263 0           my %common = (
1264             echo => $do_echo,
1265             is_child => !$is_start,
1266             );
1267            
1268 0           my $cmd;
1269            
1270 0 0         if ($$cur =~ /\Gif\b/igc) {
    0          
    0          
    0          
1271 0           my $not = $$cur =~ /\G\s+not\b/igc;
1272 0           $cmd = {
1273             %common,
1274             type => 'if',
1275             not => $not,
1276             };
1277 0 0         if ($$cur =~ /\G\s+errorlevel\s+/igc) {
    0          
    0          
    0          
1278 0           $cmd->{mode} = 'errorlevel';
1279 0           $cmd->{content} = next_token_string $$cur;
1280             } elsif ($$cur =~ /\G\s+exist\s+/igc) {
1281 0           $cmd->{mode} = 'exist';
1282 0           $cmd->{content} = next_token_string $$cur;
1283             } elsif ($$cur =~ /\G\s+cmdextversion\s+/igc) {
1284 0           $cmd->{mode} = 'cmdextversion';
1285 0           $cmd->{content} = next_token_string $$cur;
1286             } elsif ($$cur =~ /\G\s+defined\s+/igc) {
1287 0           $cmd->{mode} = 'defined';
1288 0           $cmd->{content} = next_token_string $$cur;
1289             } else {
1290 0           $cmd->{ignorecase} = $$cur =~ /\G\s*+\/i\b/igc;
1291 0           $$cur =~ /\G\s+/gc;
1292 0           $cmd->{op1} = next_token_lhs $$cur;
1293            
1294 0 0         if ($not ? $$cur =~ /\G\s*+ (==) \s* /igcx
    0          
1295             : $$cur =~ /\G\s*+
1296             (==|(?:equ|neq|lss|leq|gtr|geq)\b)
1297             \s*
1298             /igcx
1299             ) {
1300 0           $cmd->{mode} = lc $1;
1301 0           $cmd->{op2} = next_token_string $$cur;
1302             } else {
1303 0           return $self->syn_error(
1304             'illegal if: %s', rest $cur
1305             );
1306             }
1307             }
1308             $cmd->{then} = $self->parse_cmd(
1309 0   0       $cur, { %{$stop // {}}, else => 1 }
  0            
1310             );
1311            
1312 0 0         if ($$cur =~ /\Gelse\b/igc) {
1313 0           $cmd->{else} = $self->parse_cmd($cur, $stop);
1314             }
1315             } elsif ($$cur =~ /\Gfor\s+/igc) {
1316 0           $cmd = {
1317             %common,
1318             type => 'for',
1319             };
1320 0 0         if ($$cur =~ /\G\/d\b/igc) {
    0          
    0          
    0          
    0          
1321             # /D
1322 0           $cmd->{mode} = 'directories';
1323             } elsif ($$cur =~ /\G\/r\b/igc) {
1324             # /R
1325 0           $cmd->{mode} = 'recursive';
1326 0           $$cur =~ /\G\s+/gc;
1327 0           $cmd->{scandir} = next_token_string $$cur;
1328             } elsif ($$cur =~ /\G\/l\b/igc) {
1329             # /L
1330 0           $cmd->{mode} = 'numbers';
1331             } elsif ($$cur =~ /\G\/f\s++(?=\%)/igc) {
1332             # /F %%v
1333 0           $cmd->{mode} = 'lines';
1334 0           $cmd->{line_options} = '';
1335             } elsif ($$cur =~ /\G\/f\s+/igc) {
1336 0 0         if (defined(my $opts = next_token_string $$cur)) {
1337             # /F opts %%v
1338 0           $cmd->{mode} = 'lines';
1339 0           $cmd->{line_options} = $opts;
1340             } else {
1341 0           return $self->syn_error(
1342             'invalid for /f: %s', rest $$cur
1343             );
1344             }
1345             } else {
1346 0           $cmd->{mode} = 'files';
1347             }
1348            
1349             # in ( ...\n ...\n ...\n ... ) do
1350             # in ( \n\n ` \n\n cmd \n\n ` \n\n ) do
1351 0 0         $$cur =~ /\G\s*+\%([a-zA-Z])\s++in\s++\(/igc
1352             or return $self->syn_error(
1353             'invalid for token %s', rest $cur
1354             );
1355 0           $cmd->{var} = $1;
1356            
1357 0           $cmd->{list} = '';
1358 0           while (1) {
1359             # ) in quotes don't terminate the list
1360 0 0         if ($$cur =~
1361             /\G( (?: [^\)\"\^]+ | $re_quotesc )+ )/xgco
1362             ) {
1363 0           $cmd->{list} .= $1;
1364             }
1365 0 0         if ($$cur =~ /\G([^\)]*+)\)/gc) {
1366 0           $cmd->{list} .= $1;
1367 0           last;
1368             }
1369            
1370 0           $cmd->{list} .= rest($cur)."\n";
1371            
1372             $self->get_next_line($cur, 'continuation')
1373             or return $self->syn_error(
1374             'invalid for list %s', $cmd->{list}
1375 0 0         );
1376             }
1377            
1378 0 0         $$cur =~ /\G\s*do\s+/igc
1379             or return $self->syn_error(
1380             'invalid for token %s', rest $cur
1381             );
1382            
1383 0           $cmd->{command} = $self->parse_cmd($cur, $stop);
1384             } elsif ($$cur =~ /\G\(/gc) {
1385 0           my @subcmd;
1386             # 'else' has no power inside the ()
1387 0           while (my $sub = $self->parse_cmd($cur, { ')' => 1 })) {
1388 0 0         $sub eq ')' and last;
1389 0 0         push @subcmd, $sub
1390             if ref $sub;
1391            
1392 0 0         $$cur =~ /\G\s*+\)/gc
1393             and last;
1394            
1395 0 0         $self->get_next_line($cur, 'continuation')
1396             or return $self->syn_error(') expected');
1397             }
1398            
1399 0           $$cur =~ /\G\s*/gc;
1400 0           my $rest = trace_statement $cur, $stop;
1401            
1402 0           $cmd = {
1403             %common,
1404             type => '(',
1405             commands => \@subcmd,
1406             redirect => $rest,
1407             };
1408             } elsif (my $statement = trace_statement $cur, $stop) {
1409 0           $cmd = {
1410             %common,
1411             type => 'simple',
1412             command => $statement,
1413             };
1414             }
1415            
1416 0 0         if ($$cur =~ /\G\s*+(
1417             (?:(?!\^)\|\|?) |
1418             (?:(?!\^)\&\&?)
1419             )/igcx) {
1420 0 0         if ($1 eq '|') {
1421 0           $cmd->{pipe_to} = $self->parse_cmd($cur, $stop);
1422             } else {
1423 0 0         $cmd->{next_if} = $1 eq '&' ? undef
    0          
1424             : $1 eq '&&' ? 'ok' : 'not ok';
1425 0           $cmd->{next_cmd} = $self->parse_cmd($cur, $stop);
1426             }
1427             }
1428            
1429 0           return $cmd;
1430             }
1431            
1432             sub setup_redirects {
1433 0     0 0   my ($self, $str) = @_;
1434            
1435 0 0         $str !~ /[\<\>]/
1436             and return ($str, '');
1437            
1438             # < > >> 2>
1439            
1440 0           my ($filtered, $redirects) = ('', '');
1441 0           my $spacer = '';
1442            
1443 0           while (1) {
1444 0           my $token = next_token_no_pipe $str;
1445 0 0 0       defined $token && $token ne ''
1446             or last;
1447            
1448 0 0         if ($token =~ /^ ( \< | \d?\>\>? ) (.*) /ix) {
1449 0           my ($op, $val) = ($1, $2);
1450 0 0         $val =~ /^\s*+$/ and $val = next_token_no_pipe $str;
1451 0 0 0       ($val // '') =~ /^\s*+$/
1452             and return [
1453             $self->syn_error(
1454             'missing operand for %s: %s',
1455             $op, $str
1456             )
1457             ];
1458            
1459 0           my $fn = unquote_token $val;
1460            
1461 0           my ($fd, $fh);
1462 0 0         if ($op eq '<') {
    0          
1463 0           $fd = 0;
1464 0 0         open $fh, '<:crlf', $self->unc2sys($fn)
1465             or return [ $self->os_error($fn, '<') ];
1466             } elsif ($op =~ /^(\d)?(\>\>?)$/) {
1467 0   0       $fd = $1 // 1;
1468 0           $op = $2;
1469            
1470 0           my $ok;
1471 0 0         if ($fn =~ /^\&(\d?)/) {
1472             # TODO: support more handles
1473 0 0         if ($1 eq '1') {
1474 0           $ok = open $fh, '>&',
1475             \*STDOUT;
1476             } else {
1477 0           $ok = open $fh, '>:crlf',
1478             $self->unc2sys('NUL');
1479             }
1480             } else {
1481             # need to add :crlf ?
1482 0           $ok = open $fh, "$op:crlf",
1483             $self->unc2sys($fn);
1484             }
1485 0 0         $ok or return [ $self->os_error($fn, $op) ];
1486             } else {
1487 0           $self->internal_error(
1488             "unknown redirect operation '%s'", $op
1489             );
1490             }
1491 0           $self->set_handle($fd => $fh);
1492            
1493 0           $redirects .= " $fd$op$fn";
1494             # need to insert spacer for e.g.
1495             # sort /r>bla blubb
1496 0           $spacer = ' ';
1497             } else {
1498 0           $filtered .= $spacer.$token;
1499 0           $spacer = '';
1500             }
1501             }
1502            
1503 0           return ($filtered, $redirects);
1504             }
1505            
1506             sub print_prompt {
1507 0     0 0   my ($self, @str) = @_;
1508 0           $self->print_stdout("\n", $self->get_prompt, @str);
1509             }
1510            
1511             sub run_single_cmd {
1512 0     0 0   my ($self, $cmd) = @_;
1513            
1514 0 0         if ('' eq ref $cmd) {
    0          
    0          
    0          
    0          
1515 0 0 0       if ($cmd eq 'return' && @{$self->{call_stack}}) {
  0            
1516             # copied from 'goto'...
1517 0           my $call = pop @{$self->{call_stack}};
  0            
1518            
1519 0 0         if ($call->{recursion}) {
1520 0           return 'return';
1521             } else {
1522 0           $self->{pos} = $call->{pos};
1523            
1524 0           my $saved = $call->{saved};
1525 0           $self->{keys %$saved} = values %$saved;
1526            
1527 0           return 'next';
1528             }
1529             }
1530 0           return $cmd;
1531             } elsif ($cmd->{type} eq 'if') {
1532 0           my $true;
1533 0           my $mode = $cmd->{mode};
1534             my $content = !exists $cmd->{content} ? undef
1535             : unquote_token $self->replace_late_variables(
1536             $cmd->{content}
1537 0 0         );
1538            
1539 0 0         my $str = defined $content ? "$mode $content" : undef;
1540 0 0         if ($mode eq 'errorlevel') {
    0          
    0          
    0          
1541 0 0 0       defined $content && $content =~ /^\d++$/
1542             or return $self->syn_error(
1543             "'%s' is not a number", $content
1544             );
1545 0           $true = $self->{exit_value} >= $content;
1546             } elsif ($mode eq 'exist') {
1547 0           $true = -e $self->unc2sys($content);
1548             } elsif ($mode eq 'cmdextversion') {
1549 0 0 0       defined $content && $content =~ /^\d++$/
1550             or return $self->syn_error(
1551             "'%s' is not a number", $content
1552             );
1553 0           $true = 1 <= $content;
1554             } elsif ($mode eq 'defined') {
1555 0           $true = '' ne $self->{vars}{uc $content};
1556             } else {
1557 0           my $uc = $cmd->{ignorecase};
1558 0           my ($op1, $op2) = @$cmd{qw(op1 op2)};
1559 0           ($op1, $op2) = map unquote_token(
1560             $self->replace_late_variables($_)
1561             ), $op1, $op2;
1562            
1563 0 0         $str = ($uc ? '/I ' : '')."\"$op1\" $mode \"$op2\"";
1564            
1565 0 0         if ($mode eq '==') {
1566 0           $true = $op1 eq $op2;
1567             } else {
1568 0 0         if ("$op1$op2" =~ /^\d++$/) {
1569 0 0         $true = $mode eq 'equ' ? $op1 == $op2
    0          
    0          
    0          
    0          
    0          
1570             : $mode eq 'neq' ? $op1 != $op2
1571             : $mode eq 'lss' ? $op1 < $op2
1572             : $mode eq 'leq' ? $op1 <= $op2
1573             : $mode eq 'gtr' ? $op1 > $op2
1574             : $mode eq 'geq' ? $op1 >= $op2
1575             : 0
1576             } else {
1577 0 0         $true = $mode eq 'equ' ? $op1 eq $op2
    0          
    0          
    0          
    0          
    0          
1578             : $mode eq 'neq' ? $op1 ne $op2
1579             : $mode eq 'lss' ? $op1 lt $op2
1580             : $mode eq 'leq' ? $op1 le $op2
1581             : $mode eq 'gtr' ? $op1 gt $op2
1582             : $mode eq 'geq' ? $op1 ge $op2
1583             : 0
1584             }
1585             }
1586             }
1587 0 0         $cmd->{not} and $true = !$true;
1588            
1589             $cmd->{echo} and $self->print_prompt(
1590 0 0         'if ', $cmd->{not} ? 'not ' : '', $str
    0          
1591             );
1592            
1593 0 0         if ($true) {
    0          
1594 0           return $self->run_cmd($cmd->{then});
1595             } elsif ($cmd->{else}) {
1596 0           return $self->run_cmd($cmd->{else});
1597             }
1598 0           return 'next';
1599             } elsif ($cmd->{type} eq 'for') {
1600 0           my $var = $cmd->{var};
1601 0           my $command = $cmd->{command};
1602 0           my $mode = $cmd->{mode};
1603 0           my $list = $self->replace_late_variables($cmd->{list});
1604 0           $list =~ s/\n/ /g;
1605 0           $list =~ /^\s*/g;
1606            
1607             my $handle_token =
1608             $mode !~ /^(?:files|directories|recursive)$/ ? undef
1609             : sub {
1610 0     0     my ($token) = @_;
1611            
1612 0           my $is_glob = $token =~ /[\*\?]/;
1613 0           my @for;
1614 0 0 0       if (!$is_glob && $mode ne 'recursive') {
1615 0           @for = (trim_quoted $token);
1616             } else {
1617 0           @for = sort $self->filter_glob($mode,
1618             $is_glob, unquote_token $token
1619             );
1620             }
1621            
1622 0           for (@for) {
1623 0           local $self->{for_vars}{$var} = $_;
1624             #dump $command;
1625 0           my $result = $self->run_cmd($command);
1626 0 0         $result eq 'next' or return $result;
1627             }
1628 0 0         };
1629            
1630 0 0 0       if ($mode eq 'files' || $mode eq 'directories') {
    0          
    0          
    0          
1631            
1632 0 0         $cmd->{echo} and $self->print_prompt('for ',
    0          
1633             $mode eq 'directories' ? '/d ' : '',
1634             "%$var in (", $list, ')'
1635             );
1636            
1637 0           while (my $token = next_token $list) {
1638 0           $handle_token->($token);
1639             }
1640             } elsif ($mode eq 'recursive') {
1641 0           my $scandir = unquote_token $cmd->{scandir};
1642            
1643 0 0         $cmd->{echo} and $self->print_prompt('for ',
1644             '/r ', $scandir, " %$var in (", $list, ')'
1645             );
1646            
1647 0           my @token;
1648 0           push @token, $_ while defined($_ = next_token $list);
1649            
1650             my $wanted = sub {
1651 0 0   0     -d or return;
1652 0           my $dir = $self->sys2unc($_);
1653             $handle_token->(File::Spec::Win32->catfile(
1654             $dir, $_
1655 0           )) for @token;
1656 0           };
1657            
1658             find {
1659             no_chdir => 1, wanted => $wanted,
1660             preprocess => sub {
1661 0     0     return sort @_;
1662             },
1663 0           }, $self->unc2sys($scandir);
1664            
1665             } elsif ($mode eq 'numbers') {
1666 0           my ($start, $step, $end) = split /,/, $list;
1667 0           s/^\s+//s for $start, $step, $end;
1668 0           s/\s+$//s for $start, $step, $end;
1669             /^-?\d+$/ or
1670             return $self->syn_error(
1671             "for: '%s' is not a number", $_
1672             )
1673 0   0       for $start, $step, $end;
1674            
1675 0 0         $cmd->{echo} and $self->print_prompt('for ',
1676             "/l %$var in ($start, $step, $end)"
1677             );
1678            
1679 0 0         for (my $i = $start;
    0          
1680             $step > 0 ? $i <= $end
1681             : $step < 0 ? $i >= $end : 0;
1682             $i += $step
1683             ) {
1684            
1685 0           local $self->{for_vars}{$var} = $i;
1686 0           my $result = $self->run_cmd($command);
1687 0 0         $result eq 'next' or return $result;
1688             }
1689             } elsif ($mode eq 'lines') {
1690 0           my $opts = unquote_token $cmd->{line_options};
1691            
1692 0 0         $cmd->{echo} and $self->print_prompt('for ',
1693             "/f \"$opts\" %$var in (", $list, ')'
1694             );
1695            
1696 0           my %opt;
1697 0           (@opt{qw(eol skip delims tokens)}, my $usebackq) = (
1698             undef, 0, "\t ", '1', 0
1699             );
1700 0 0         $opts =~ /\beol=(.)/i and $opt{eol} = $1;
1701 0 0         $opts =~ /\bskip=(\d+)/i and $opt{skip} = $1;
1702 0 0         $opts =~ /\bdelims=(.?[^\s]*)/i and $opt{delims} = $1;
1703             $opts =~ /\btokens=(\d[\d,-]*+\*?)/i
1704 0 0         and $opt{tokens} = $1;
1705 0 0         $opts =~ /\busebackq\b/i and $usebackq = 1;
1706            
1707             $opt{eol} and $opt{eol_re} =
1708 0 0         qr/^([\Q$opt{eol}\E]+)\Q$opt{eol}\E.*+$/;
1709             $opt{delims} ne ''
1710 0 0         and $opt{delim_re} = qr/[\Q$opt{delims}\E]/;
1711            
1712             my @token = map {
1713 0 0         /^(\d+)\-(\d+)$/ ? ($1 .. $2) : ($_);
1714 0           } split /,/, $opt{tokens};
1715            
1716 0 0         if ($token[-1] =~ s/\*$//) {
1717             # 1,* or 1*
1718 0 0         $token[-1] eq '' and pop @token;
1719 0           push @token, $token[-1]+1;
1720 0           $opt{numvals} = @token;
1721             } else {
1722 0           $opt{numvals} = @token+1;
1723             }
1724            
1725             /^\d++$/ or
1726             return $self->syn_error(
1727             "invalid token '%s'", $_
1728             )
1729 0   0       for @token;
1730 0           $opt{tokens} = [ map $_-1, @token ];
1731            
1732 0           my @var;
1733 0           my $varcount = "$var";
1734 0           push @var, $varcount++ for 1..$opt{numvals};
1735            
1736 0           my $type = 'files';
1737 0 0 0       if ( # $usebackq &&
    0 0        
      0        
1738             $list =~ /^\s*\"/s && $list =~ /\"\s*+$/s
1739             ) {
1740 0           $type = 'explicit';
1741 0           $list =~ s/^\s*+\"//s;
1742 0           $list =~ s/\"\s*+$//s;
1743             } elsif ($usebackq &&
1744             $list =~ /^\s*\`/s && $list =~ /\`\s*+$/s
1745             ) {
1746 0           $type = 'output';
1747 0           $list =~ s/^\s*+\`//s;
1748 0           $list =~ s/\`\s*+$//s;
1749             }
1750            
1751             local $self->{for_vars} = my $for_vars = {
1752 0           %{$self->{for_vars}}
  0            
1753             };
1754 0 0         if ($type eq 'explicit') {
    0          
1755 0 0         if (my @val = extract_for_tokens \%opt, $list) {
1756 0           @$for_vars{@var} = @val;
1757 0           my $result = $self->run_cmd($command);
1758 0 0         $result eq 'next' or return $result;
1759             }
1760             } elsif ($type eq 'files') {
1761 0           $list =~ /^\s+/gc;
1762 0           while (my $token =
1763             unquote_token next_token $list
1764             ) {
1765 0 0         open my $fh, '<:crlf',
1766             $self->unc2sys($token)
1767             or return $self->os_error(
1768             $token, 'for'
1769             );
1770 0           while (<$fh>) {
1771 0           chomp;
1772 0 0         my @val = extract_for_tokens
1773             \%opt, $_
1774             or next;
1775 0           @$for_vars{@var} = @val;
1776 0           my $result = $self->run_cmd(
1777             $command
1778             );
1779 0 0         $result eq 'next'
1780             or return $result;
1781             }
1782             }
1783             } else {
1784 0           my $pipe = File::Temp->new();
1785 0           open my $old_stdout, '>&', \*STDOUT;
1786 0           open STDOUT, '>&', $pipe;
1787            
1788             # clone to be safe about modification
1789 0           my $result = $self->deep_clone->run_script(
1790             # no arguments, but just in case...
1791             { quoted_arguments => 1 },
1792             [ $list ]
1793             );
1794 0 0         $result ne 'next' and return $result;
1795            
1796 0           open STDOUT, '>&', $old_stdout;
1797            
1798 0           seek $pipe, 0, SEEK_SET;
1799 0           while (<$pipe>) {
1800 0           chomp;
1801 0 0         my @val = extract_for_tokens \%opt, $_
1802             or next;
1803 0           @$for_vars{@var} = @val;
1804 0           my $result = $self->run_cmd($command);
1805 0 0         $result eq 'next'
1806             or return $result;
1807             }
1808             }
1809             }
1810 0           return 'next';
1811             } elsif ($cmd->{type} eq '(') {
1812             my ($rest, $redirects) =
1813 0   0       $self->setup_redirects($cmd->{redirect} // '');
1814 0 0         'ARRAY' eq ref $rest and return $rest->[0];
1815 0 0         $rest eq '' or return $self->syn_error(
1816             '(): rest after redirects: %s', $rest
1817             );
1818            
1819             $cmd->{echo} and
1820 0 0         $self->print_prompt('()', $redirects);
1821            
1822 0           my $result;
1823 0           for my $sub (@{$cmd->{commands}}) {
  0            
1824 0           $result = $self->run_cmd($sub);
1825             # TODO: too simple for nested calls?
1826 0 0         $result eq 'next' or return $result;
1827             }
1828 0           return $result;
1829             } elsif ($cmd->{type} ne 'simple') {
1830             return $self->syn_error(
1831             'unknown command type %s', $cmd->{type}
1832 0           );
1833             }
1834            
1835 0           my ($cur, $redirects) = $self->setup_redirects($cmd->{command});
1836 0 0         'ARRAY' eq ref $cur and return $cur->[0];
1837            
1838 0           $cur = $self->replace_late_variables($cur);
1839            
1840 0           $cur =~ /\G\s*/gc;
1841 0 0         $cur =~ /\G$/gc
1842             and return 'empty';
1843            
1844             # be echo compatible at least for echo Hello World! ...
1845             $cmd->{echo} and
1846 0 0 0       $self->print_prompt($cur, $redirects || ' ');
1847            
1848 0           my ($qcommand, $args, @qarg);
1849             {
1850 0           my $pos = pos $cur;
  0            
1851            
1852 0           $qcommand = next_token $cur;
1853 0 0         defined $qcommand or return $self->syn_error(
1854             'no command in %s', rest \$cur
1855             );
1856            
1857 0           $args = rest \$cur;
1858            
1859 0           while (defined(my $arg = next_token $cur)) {
1860 0           push @qarg, $arg;
1861             }
1862            
1863 0           pos $cur = $pos;
1864             }
1865 0           my @arg = map unquote_token($_), @qarg;
1866            
1867 0           my $lc_qcommand = lc $qcommand;
1868 0           $lc_qcommand =~ s/\s++$//;
1869            
1870             # internal commands can't be quoted
1871 0 0 0       if ($lc_qcommand eq 'call') {
    0          
    0          
1872 0 0 0       if (@arg && $arg[0] =~ /^\:(\w+)$/) {
1873 0           my $label = $1;
1874            
1875 0           my $index = $self->{index}{uc $label};
1876 0 0         defined $index or
1877             return $self->syn_error(
1878             'no call label %s', $label
1879             );
1880            
1881 0           my @save = qw(args orig_args);
1882             my %saved = (
1883 0           map +($_ => $self->{$_}), @save
1884             );
1885            
1886 0           my @carg;
1887 0           while (defined(my $arg = next_token_call_arg $args)) {
1888 0           push @carg, $arg;
1889             }
1890            
1891 0           $self->{args} = \@carg;
1892 0           $self->{orig_args} = [ @qarg[1..$#qarg] ];
1893            
1894 0 0         if ($cmd->{is_child}) {
1895 0           my $pos = $self->{pos};
1896 0           push @{$self->{call_stack}}, {
  0            
1897             recursion => 1,
1898             };
1899 0           $self->{pos} = $index;
1900 0           my $result = $self->run_block;
1901             #say "RETURNED: $pos $result";
1902 0           $self->{pos} = $pos;
1903 0           @$self{@save} = @saved{@save};
1904 0 0         $result eq 'return' or
1905             return $self->internal_error(
1906             'illegal return %s', $result
1907             );
1908             } else {
1909 0           push @{$self->{call_stack}}, {
1910             saved => \%saved,
1911             pos => $self->{pos},
1912 0           };
1913 0           $self->{pos} = $index;
1914             }
1915 0           return 'next';
1916             } else {
1917 0           $args = substr $args, length $qcommand;
1918            
1919 0           $qcommand = shift @qarg;
1920 0           shift @arg;
1921 0 0         if (defined $qcommand) {
1922 0           $lc_qcommand = lc $qcommand;
1923 0           $lc_qcommand =~ s/\s++$//;
1924             } else {
1925 0           undef $lc_qcommand;
1926             }
1927            
1928             # fallthrough to external command
1929             }
1930             } elsif ($lc_qcommand eq 'goto' || $lc_qcommand eq 'exit') {
1931 0           my $return; # goto :EOF or exit /b
1932             my $label;
1933            
1934 0 0         if ($lc_qcommand eq 'goto') {
1935 0           $args =~ /\G\:?(\w+)/gc;
1936 0   0       $label = $1 // 'error';
1937 0           $return = 'EOF' eq uc $label;
1938             } else {
1939 0           my $b = $args =~ /\G\/b\b/igc;
1940             #say "EXIT???";
1941 0 0         $b or return 'exit';
1942 0           $return = 1;
1943             }
1944            
1945 0 0         if ($return) {
1946             #say "RETURN: @{$self->{call_stack}}";
1947 0 0         @{$self->{call_stack}} or return 'exitb';
  0            
1948            
1949             # undo call
1950 0           my $call = pop @{$self->{call_stack}};
  0            
1951            
1952 0 0         if ($call->{recursion}) {
1953 0           return 'return';
1954             } else {
1955 0           $self->{pos} = $call->{pos};
1956            
1957 0           my $saved = $call->{saved};
1958 0           @$self{keys %$saved} = values %$saved;
1959            
1960 0           return 'next';
1961             }
1962             }
1963            
1964 0           my $index = $self->{index}{uc $label};
1965 0 0         defined $index or
1966             return $self->syn_error('no goto label %s', $label);
1967 0           $self->{pos} = $index;
1968 0           return 'next';
1969             } elsif ($cur =~ /\Gecho\b/igc) {
1970             # very special parsing rules
1971 0 0         if ($cur =~ /\G\s*+$/gc) {
    0          
    0          
    0          
    0          
1972             say $self->get_message(
1973 0 0         $self->{echo} ? 'ECHO is ON.' : 'ECHO is OFF.'
1974             );
1975             } elsif ($cur =~ /\G\s+on\s*+$/gci) {
1976 0           $self->{echo} = 1;
1977             } elsif ($cur =~ /\G\s+off\s*+$/gci) {
1978 0           $self->{echo} = 0;
1979             } elsif ($cur =~ /\G\.\s*+$/gc) {
1980 0           say "";
1981             } elsif ($cur =~ /\G\s/gc) {
1982 0           say unescape rest \$cur;
1983             } else {
1984 0           return $self->syn_error('invalid echo %s', rest \$cur);
1985             }
1986 0           return 'next';
1987             }
1988            
1989 0           my $command = unquote_token $qcommand;
1990            
1991 0 0         if (my $interpreter = $self->get_interpreter_for_extension(
1992             $command, \@arg, $qcommand, \@qarg
1993             )) {
1994 0           $command = $interpreter;
1995 0           $args = join '', @qarg;
1996             }
1997            
1998 0           my $turn;
1999             my %add_env;
2000             retry:
2001 0           for my $handlers (@$self{qw(internal_commands external_commands)}) {
2002 0           my $handler;
2003 0 0         my $key = $turn ? lc($command) : $lc_qcommand;
2004 0 0 0       unless (exists $handlers->{$key} &&
2005             defined($handler = $handlers->{$key})
2006             ) {
2007 0 0         $turn++ > 100 and return $self->internal_error(
2008             'recursive handlers for %s', $key
2009             );
2010 0           next;
2011             }
2012            
2013 0 0         if ('CODE' eq ref $handler) {
    0          
2014 0           my $ret = $handler->($self,
2015             $command, \@arg, \@qarg, $args,
2016             );
2017 0 0 0       if ('HASH' eq ref $ret) {
    0          
2018 0 0         if ($ret->{type} eq 'rewrite') {
2019 0           $command = $ret->{command};
2020 0           $args = join '', @qarg;
2021             $ret->{env} and
2022 0           @add_env{keys %{$ret->{env}}}
2023 0 0         = values %{$ret->{env}};
  0            
2024 0           goto retry;
2025             }
2026             } elsif ('' eq ref $ret && defined $ret) {
2027 0 0         $ret =~ /^error/ and return $ret;
2028             $ret eq 'keep' or
2029 0 0         $self->{exit_value} = $ret;
2030 0           return 'next';
2031             }
2032 0           return $self->internal_error(
2033             'unknown handler result: %s', dump $ret
2034             );
2035             } elsif ('' eq ref $handler) {
2036 0           $command = $handler;
2037             }
2038            
2039             }
2040            
2041 0           my $ret = $self->run_external_command(
2042             { add_to_env => \%add_env },
2043             $self->exe2sys($command),
2044             map $self->unc2sys_arg($_), @arg
2045             );
2046 0 0         $ret =~ /^error/ and return $ret;
2047             $ret eq 'keep' or
2048 0 0         $self->{exit_value} = $ret;
2049 0           return 'next';
2050             }
2051            
2052             sub exe2sys {
2053 0     0 0   my ($self, $command) = @_;
2054            
2055 0           my $exe = $self->unc2sys($command);
2056 0 0 0       unless ($self->{assume_is_executable}{uc $exe} || -f $exe) {
2057 0   0       $command = $self->find_in_path('PATH', $command) // $command;
2058 0           $exe = $self->unc2sys($command);
2059             }
2060            
2061 0           return $exe;
2062             }
2063            
2064             sub get_interpreter_for_extension {
2065 0     0 0   my ($self, $command, $args, $qcommand, $qargs) = @_;
2066            
2067 0 0         $command =~ /\.(\w)$/
2068             or return;
2069 0           my $ext = lc $1;
2070            
2071 0 0         my $handler = $self->{extension_handlers}{$ext}
2072             or return;
2073            
2074 0 0         my $found_command = -f $self->unc2sys($command) ? $command
2075             : $self->find_in_path('PATH', $command);
2076            
2077 0 0         my ($interpreter, @addarg) =
2078             $handler->($self, $found_command, $args, $qcommand, $qargs)
2079             or return;
2080 0 0         defined $interpreter
2081             or return;
2082            
2083 0           unshift @$args, @addarg;
2084 0           unshift @$qargs, map requote($_), @addarg;
2085            
2086 0           return $interpreter;
2087             }
2088            
2089             =head2 ->run_external_command($attr, $exe, @arg)
2090            
2091             Runs the external program C<$exe> with command line arguments C<@arg> and an optionally changed environment, while optionally translating the input and output text format, and return a result compatible with the Command Handler interface.
2092            
2093             C<$attr> supports the following keys:
2094            
2095             =over
2096            
2097             =item add_to_env
2098             A hash of additional environment variables that will be set temporarily while calling the program.
2099            
2100             =item flip_stdin
2101             Convert the newlines of STDIN from crlf to the host format.
2102            
2103             =item flip_stdout
2104             Convert the newlines of STDOUT from the host format to crlf.
2105            
2106             =item flip_stderr
2107             Convert the newlines of STDERR from the host format to crlf.
2108            
2109             =back
2110            
2111             =cut
2112             sub run_external_command {
2113 0     0 1   my ($self, $attr, $exe, @arg) = @_;
2114            
2115 0           my $result;
2116 0   0       my %old_env = $self->set_subprocess_env($attr->{add_to_env} // {});
2117 0 0         if ($is_win_host) {
2118             # windows has no system LIST
2119 0           my $commandline = join ' ', map quote_native($_), $exe, @arg;
2120 0 0         $self->{verbose_system} and say STDERR "SYSTEM $commandline";
2121 0           $result = system $commandline;
2122             # TODO: non-existent commands result in exit value 9009
2123 0           $result >>= 8;
2124             } else {
2125             $self->{verbose_system} and
2126 0 0         say STDERR 'SYSTEM '.join ' ', $exe, @arg;
2127            
2128 0           local $self->{saved_handles};
2129 0           my @wait;
2130 0 0 0       if ($attr->{flip_stdin} && !defined $result) {
2131 0           pipe my ($reader, $writer);
2132 0           my $pid = fork;
2133 0 0 0       unless ($pid // 1) {
2134 0           close $reader;
2135 0           binmode STDIN, ':crlf'; # to be sure
2136 0           binmode $writer;
2137 0           autoflush $writer;
2138 0           print $writer $_ while ;
2139 0           exit 0;
2140             }
2141            
2142 0 0         unless (defined $pid) {
2143 0           $result = "error: fork: $!";
2144             } else {
2145 0           close $writer;
2146 0           $self->set_handle(STDIN => $reader);
2147 0           push @wait, $pid;
2148             }
2149             }
2150 0 0 0       if ($attr->{flip_stdout} && !defined $result) {
2151 0           pipe my ($reader, $writer);
2152 0           my $pid = fork;
2153 0 0 0       unless ($pid // 1) {
2154 0           close $writer;
2155 0           binmode $reader;
2156 0           binmode STDOUT, ':crlf'; # to be sure
2157 0           autoflush STDOUT;
2158 0           print STDOUT $_ while <$reader>;
2159 0           exit 0;
2160             }
2161            
2162 0 0         unless (defined $pid) {
2163 0           $result = "error: fork: $!";
2164             } else {
2165 0           close $reader;
2166 0           $self->set_handle(STDOUT => $writer);
2167 0           push @wait, $pid;
2168             }
2169             }
2170 0 0 0       if ($attr->{flip_stderr} && !defined $result) {
2171 0           pipe my ($reader, $writer);
2172 0           my $pid = fork;
2173 0 0 0       unless ($pid // 1) {
2174 0           close $writer;
2175 0           binmode $reader;
2176 0           binmode STDERR, ':crlf'; # to be sure
2177 0           autoflush STDERR;
2178 0           print STDERR $_ while <$reader>;
2179 0           exit 0;
2180             }
2181            
2182 0 0         unless (defined $pid) {
2183 0           $result = "error: fork: $!";
2184             } else {
2185 0           close $reader;
2186 0           $self->set_handle(STDERR => $writer);
2187 0           push @wait, $pid;
2188             }
2189             }
2190            
2191 0 0         unless (defined $result) {
2192 0           $result = system $exe $exe, @arg;
2193 0           $result >>= 8;
2194             }
2195            
2196 0           $self->reset_handles;
2197            
2198             # wait for pipes to be flushed
2199 0           waitpid($_, 0) for @wait;
2200             }
2201 0           %ENV = %old_env;
2202            
2203 0           return $result;
2204             }
2205            
2206             sub command_usage {
2207 0     0 0   my ($self, $usage) = @_;
2208 0           say STDERR "usage: $usage";
2209 0           return 'keep';
2210             }
2211            
2212             sub command_error {
2213 0     0 0   my ($self, @arg) = @_;
2214 0           say STDERR $self->syn_error(@arg);
2215 0           return 'keep';
2216             }
2217            
2218             sub command_os_error {
2219 0     0 0   my ($self, @arg) = @_;
2220 0           say STDERR $self->os_error(@arg);
2221 0           return 'keep';
2222             }
2223            
2224             sub command_chdir {
2225 0     0 0   my ($self, $command, $args) = @_;
2226            
2227 0 0 0       my $drive = lc($args->[0] // '') eq '/d'
2228             and shift @$args;
2229 0 0         if (!@$args) {
    0          
2230 0           say $self->get_curdir;
2231             } elsif (@$args > 1) {
2232 0           return $self->command_usage("$command ");
2233             }
2234            
2235 0           my $dir = $args->[0];
2236 0 0         $self->set_curdir($dir)
2237             or return $self->command_os_error($dir, 'chdir');
2238            
2239 0           return 'keep';
2240             }
2241            
2242             sub command_delete {
2243 0     0 0   my ($self, $command, $args) = @_;
2244            
2245 0 0         @$args == 1 or return $self->command_usage("$command ");
2246 0           my $glob = $args->[0];
2247            
2248             /[\?\*]/ and return $self->command_error("%s: is a glob", $_)
2249 0   0       for $glob;
2250            
2251 0           my $file = $glob;
2252 0 0         unlink $self->unc2sys($file)
2253             or return $self->command_os_error($file, 'unlink');
2254            
2255 0           return 'keep';
2256             }
2257            
2258             sub command_mkdir {
2259 0     0 0   my ($self, $command, $args) = @_;
2260            
2261 0 0         @$args == 1 or return $self->command_usage("$command ");
2262 0           my $dir = $args->[0];
2263            
2264 0           my ($vol, $dirs, $file) =
2265             File::Spec::Win32->splitpath($dir, 'no_file');
2266            
2267 0           my @path;
2268             my $ok;
2269 0           for my $level (File::Spec::Win32->splitdir($dir)) {
2270 0           push @path, $level;
2271 0           my $path = File::Spec::Win32->catpath(
2272             $vol, File::Spec::Win32->catdir(@path)
2273             );
2274 0           $ok = mkdir $self->unc2sys($path);
2275             }
2276 0 0         $ok or return $self->command_os_error($dir, 'mkdir');
2277            
2278 0           return 'keep';
2279             }
2280            
2281             sub command_move_copy {
2282 0     0 0   my ($self, $command, $args) = @_;
2283            
2284 0           my $src = $args->[0];
2285 0           my $dest = $args->[1];
2286 0 0 0       defined $src && defined $dest && !@$args
      0        
2287             or return $self->command_usage("$command ");
2288             /[\?\*]/ and return $self->commanderror("%s: is a glob", $_)
2289 0   0       for $src, $dest;
2290 0 0         -e $dest and return $self->command_error(
2291             "%s: exists, won't overwrite", $dest
2292             );
2293            
2294 0 0         if (lc $command eq 'copy') {
2295             # syscopy?
2296 0 0         copy map $self->unc2sys($_), $src, $dest
2297             or return $self->command_os_error($src, 'copy');
2298             } else {
2299 0 0         move map $self->unc2sys($_), $src, $dest
2300             or return $self->command_os_error($src, 'move');
2301             }
2302            
2303 0           return 'keep';
2304             }
2305            
2306             sub command_rmdir {
2307 0     0 0   my ($self, $command, $args) = @_;
2308            
2309 0 0         @$args == 1 or return $self->command_usage("$command ");
2310 0           my $glob = $args->[0];
2311            
2312             /[\?\*]/ and return $self->command_error('%s: is a glob', $_)
2313 0   0       for $glob;
2314            
2315 0           my $file = $glob;
2316 0 0         rmdir $self->unc2sys($file)
2317             or return $self->command_os_error($file, 'rmdir');
2318            
2319 0           return 'keep';
2320             }
2321            
2322             sub command_set {
2323 0     0 0   my ($self, $command, undef, undef, $cur) = @_;
2324            
2325 0 0         if ($cur =~ /\G\s*+([^\=\s]*+)\s*+$/gc) {
2326 0 0         my $filter = $1 ? qr/^\Q$1\E/i : undef;
2327 0           my ($vars, $cases) = @$self{qw(vars varcases)};
2328 0           my @order = sort grep exists $cases->{$_}, keys %$vars;
2329 0 0         $filter and @order = grep /$filter/, @order;
2330            
2331             say $cases->{$_}.'='.$vars->{$_}
2332 0           for @order;
2333 0           return 'keep';
2334             }
2335            
2336 0           my $arith = $cur =~ /\G\s*+\/a\b/igc;
2337 0           my $prompt = $cur =~ /\G\s*+\/p\b/igc;
2338 0 0 0       $arith && $prompt and
2339             return $self->command_error('set: /A and /P not combinable');
2340 0           my $set = rest \$cur;
2341 0           chomp $set;
2342            
2343 0 0         unless ($arith) {
2344 0 0         $set =~ s/^\s*+\"// and
2345             $set =~ s/\"[^\"]*$//;
2346            
2347 0 0         $set =~ s/^\s*+([^\=\s]++)\s*+\=//g
2348             or return $self->command_error(
2349             "syntax error in set: %s", $set
2350             );
2351 0           my $var = $1;
2352            
2353 0 0 0       $prompt and $set = $self->readline($set) // '';
2354            
2355 0           $self->set_variable($var => $set);
2356            
2357 0           return 'keep';
2358             }
2359            
2360            
2361             # () - Gruppierung
2362             # ! ~ - - mon„re Operatoren
2363             # * / % - arithmetische Operatoren
2364             # + - - arithmetische Operatoren
2365             # << >> - logische Verschiebung
2366             # & - bitweise UND
2367             # ^ - bitweise exklusives ODER
2368             # | - bitweise ODER
2369             # = *= /= %= += -= - Zuordnung
2370             # &= ^= |= <<= >>=
2371             # , - Trennzeichen fr Ausdrcke
2372            
2373 0           state $opchar = q{()!~-*/%+<>&^|};
2374 0           for my $expr (split /,/, $set) {
2375 0           $expr =~ s/\"//g;
2376            
2377 0 0         $expr =~ /\G\s*+
2378             ([^\s\*\/\%\+\-\&\^\|\<\>]+)
2379             \s*+
2380             (|\*|\/|\%|\+|\-|\&|\^|\||\<\<|\>\>)\=
2381             (.*)
2382             /gcx
2383             or return $self->command_error(
2384             "syntax error in set: '%s'", $expr
2385             );
2386 0           my ($var, $op, $rhs) = ($1, $2, $3);
2387 0 0         $op and $rhs = "$var $op ( $rhs )";
2388            
2389 0           while ($rhs =~ /\G.*?([^0-9\s\.\;\Q$opchar\E]+)/gcio) {
2390 0           my $v = $1;
2391 0           my $start = pos($rhs) - length $v;
2392 0   0       my $r = ' '.($self->{vars}{uc $v} // 0).' ';
2393 0           substr $rhs, $start, length $v, $r;
2394 0           pos $rhs = $start + length $r;
2395             }
2396 0           $rhs = rebase_numbers $rhs;
2397            
2398             # kill magic
2399 0           $rhs =~ s/([\%\&])/$1 /g;
2400            
2401             # maybe more characters behave this way...
2402 0           $rhs =~ s/\;.*$//;
2403            
2404 0 0         $rhs =~ /^[0-9\s\Q$opchar\E]+$/o
2405             or return $self->command_error(
2406             "arithmetic expression for %s".
2407             " not supported: %s", $var, $expr
2408             );
2409            
2410 0           my $val = eval $rhs;
2411 0 0         $@ and return $self->command_error(
2412             "arithmethic expression".
2413             " error while evaluating '%s': %s", $rhs, $@
2414             );
2415            
2416 0           $self->set_variable($var => $val);
2417             }
2418            
2419 0           return 'keep';
2420             }
2421            
2422             my %internal_command = (
2423             cd => \&command_chdir,
2424             chdir => \&command_chdir,
2425             copy => \&command_move_copy,
2426             date => sub {
2427             my ($self, $command, $args) = @_;
2428             lc($args->[0] // '') eq '/t'
2429             or return $self->command_error(
2430             "%s: setting not supported", $command
2431             );
2432             say $self->get_date;
2433             return 'keep';
2434             },
2435             del => \&command_delete,
2436             dir => sub {
2437             my ($self, $command, $args) = @_;
2438             my $sort = 'time';
2439             my $glob = $args->[0] // '*';
2440             my @content = map [ $_, $self->sys2unc($_) ],
2441             glob $self->unc2sys($glob);
2442             printf "%-21s%14s %s\n",
2443             $self->get_file_timedate($_->[0], 'dir'),
2444             -d $_->[0] ? ' '
2445             : $self->format_size(-s $_->[0]),
2446             $_->[1]
2447             for @content;
2448             return 'keep';
2449             },
2450             endlocal => sub {
2451             my ($self, $command, $args) = @_;
2452             if (my $old = pop @{$self->{state_stack}}) {
2453             @$self{keys %$old} = values %$old;
2454             }
2455             return 'keep';
2456             },
2457             erase => \&command_delete,
2458             md => \&command_mkdir,
2459             mkdir => \&command_mkdir,
2460             move => \&command_move_copy,
2461             pause => sub {
2462             my ($self, $command, $args) = @_;
2463             # TODO: any key is allowed, readline wants Enter
2464             $self->readline(
2465             $self->get_message('Press a key to continue . . .')
2466             );
2467             return 'keep';
2468             },
2469             popd => sub {
2470             my ($self, $command, $args) = @_;
2471             my $dir = pop @{$self->{pushd_stack}};
2472             $self->set_curdir($dir)
2473             or return $self->command_os_error($dir, $command);
2474             return 'keep';
2475             },
2476             prompt => sub {
2477             my ($self, $command, $args) = @_;
2478             $self->set_variable(PROMPT => $args->[0]);
2479             return 'keep';
2480             },
2481             pushd => sub {
2482             my ($self, $command, $args) = @_;
2483             push @{$self->{pushd_stack}}, $self->get_curdir;
2484             if (defined(my $dir = $args->[0])) {
2485             $self->set_curdir($dir)
2486             or return $self->command_os_error(
2487             $dir, $command
2488             );
2489             }
2490             return 'keep';
2491             },
2492             rd => \&command_rmdir,
2493             ren => \&command_move_copy,
2494             rename => \&command_move_copy,
2495             rmdir => \&command_rmdir,
2496             set => \&command_set,
2497             setlocal => sub {
2498             my ($self, $command, $args) = @_;
2499             my %switch;
2500            
2501             while ($args =~ /\G
2502             (en|dis)able (delayedexpansion|extensions)
2503             \s*/igcx
2504             ) {
2505             $switch{lc $2} = lc $1 eq 'en';
2506             }
2507            
2508             my @hash = qw(vars varcases var_path_cache);
2509             my @scalar = qw(delayedexpansion);
2510            
2511             push @{$self->{state_stack}}, {
2512             map +($_ => $self->{$_}), @hash, @scalar
2513             };
2514             $self->{$_} = { %{$self->{$_}} } for @hash;
2515            
2516             return %switch ? 0 : 1;
2517             },
2518             shift => sub {
2519             my ($self, $command, $args) = @_;
2520            
2521             my $fixed = 0;
2522             @$args && $args->[0] =~ /^\/([0-8])$/
2523             and $fixed = $1;
2524             splice @{$self->{args}}, $fixed, 1
2525             if $fixed < @{$self->{args}};
2526            
2527             return 'keep';
2528             },
2529             time => sub {
2530             my ($self, $command, $args) = @_;
2531             lc($args->[0] // '') eq '/t'
2532             or return $self->command_error(
2533             "%s: setting not supported", $command
2534             );
2535             say $self->get_time('short');
2536             return 'keep';
2537             },
2538             type => sub {
2539             my ($self, $command, $args) = @_;
2540            
2541             @$args == 1 or return $self->command_usage("$command ");
2542             my $filename = $args->[0];
2543             open my $fh, '<:crlf', $self->unc2sys($filename)
2544             or return $self->command_os_error($filename, $command);
2545             print while <$fh>;
2546            
2547             return 'keep';
2548             },
2549             ver => sub {
2550             my ($self, $command, $args) = @_;
2551             say $self->{version_string};
2552             $self->{exit_value} = 0;
2553             return 'keep';
2554             },
2555             );
2556            
2557             sub default_variable_cases {
2558 0     0 0   return { map +(uc $_ => $_), qw(
2559             Path ComSpec PSModulePath
2560             windir windows_tracing_flags windows_tracing_logfile
2561             CommonProgramFiles CommonProgramFiles(x86) CommonProgramW6432
2562             ProgramData ProgramFiles ProgramFiles(x86) ProgramW6432
2563             SystemDrive SystemRoot
2564             )};
2565             }
2566            
2567             sub default_filenames {
2568             return {
2569 0 0   0 1   $is_win_host ? (
2570             ) : (
2571             NUL => '/dev/null',
2572             CON => '/dev/console',
2573             ),
2574             };
2575             }
2576            
2577             sub builtin_internal_commands {
2578 0     0 1   return { %internal_command };
2579             }
2580            
2581             sub is_shortened {
2582 0     0 0   my ($pattern, $str) = @_;
2583 0           my $minlen = length $pattern;
2584 0 0         $pattern =~ s/^([^\[]*+)\[([^\]]*+)\]$/$1$2/
2585             and $minlen = length $1;
2586 0   0       return length $str >= $minlen
2587             && $str eq substr $pattern, 0, length $str;
2588             }
2589            
2590             sub translate_sort {
2591 0     0 0   my ($self, $command, $args, $qargs) = @_;
2592            
2593 0           my @opt;
2594             my %env;
2595 0           for (my $i = 0; $i < @$args; ) {
2596 0           my $arg = uc $args->[$i];
2597 0           my $rest = $#$args - $i;
2598            
2599 0           my $splice;
2600 0 0 0       if ($rest && $arg =~ /^\/\+(\d+)$/) {
    0 0        
    0 0        
    0 0        
    0 0        
    0          
    0          
2601 0           push @opt, '-k', $1;
2602 0           $splice = 1;
2603             } elsif ($rest && is_shortened '/L[OCALE]', $arg) {
2604             # case??
2605 0           $env{LC_ALL} = $args->[$i+1];
2606 0           $splice = 2;
2607             } elsif ($rest && is_shortened '/M[EMORY]', $arg) {
2608 0           push @opt, '-S', "$args->[$i+1]K";
2609 0           $splice = 2;
2610             } elsif (is_shortened '/REC[ORD_MAXIMUM]', $arg) {
2611             # ignore
2612 0           $splice = 2;
2613             } elsif (is_shortened '/R[EVERSE]', $arg) {
2614 0           push @opt, '-r';
2615 0           $splice = 1;
2616             } elsif ($rest && is_shortened '/T[EMPORARY]', $arg) {
2617 0           push @opt, '-T', $args->[$i+1];
2618 0           $splice = 2;
2619             } elsif ($rest && is_shortened '/O[UTPUT]', $arg) {
2620 0           push @opt, '-o', $args->[$i+1];
2621 0           $splice = 2;
2622             }
2623 0 0         if ($splice) {
2624 0           splice @$args, $i, $splice;
2625             #splice @$qargs, $i, $splice;
2626             } else {
2627 0           $i++;
2628             }
2629             }
2630 0           unshift @$args, @opt;
2631             #unshift @$qargs, map "$_ ", @opt;
2632            
2633 0           return $self->run_external_command(
2634             {
2635             add_to_env => \%env,
2636             flip_stdin => 1,
2637             flip_stdout => 1,
2638             },
2639             'sort', @$args
2640             );
2641             }
2642            
2643             my %extension_handler = (
2644             bat => sub {
2645             my ($self, $command, $args) = @_;
2646             # TODO (or not): replacement of current script, if not called
2647             return ($^X, $0, $command);
2648             },
2649             );
2650            
2651             sub builtin_extension_handlers {
2652 0     0 1   return { %extension_handler };
2653             }
2654            
2655             my %external_command = (
2656             mysort => \&translate_sort,
2657             $is_win_host ? () : (sort => \&translate_sort),
2658             );
2659            
2660             sub builtin_external_commands {
2661 0     0 1   return { %external_command };
2662             }
2663            
2664             sub run_single_pipe {
2665 0     0 0   my ($self, $cmd) = @_;
2666 0           my $result;
2667 0           my $wait_count = 0;
2668            
2669 0           local $self->{saved_handles} = {};
2670 0   0       while (ref $cmd && $cmd->{pipe_to}) {
2671 0           my $pseudo_pipe = $is_win_host;
2672            
2673 0           my ($reader, $writer, $pid);
2674 0 0         if ($pseudo_pipe) {
2675             # fork is emulated, fileno's are shared
2676 0           my $pipe_name = File::Temp->new();
2677 0           open $writer, '+>', $pipe_name;
2678             # '<&' doesn't work
2679 0           open $reader, '<', $pipe_name;
2680 0           $pid = 0;
2681             } else {
2682 0           pipe $reader, $writer;
2683 0           $pid = fork;
2684 0 0         defined $pid or $self->os_error(undef, 'fork');
2685             }
2686 0 0 0       unless ($pid // 1) {
2687 0 0         close $reader
2688             unless $pseudo_pipe;
2689 0           $self->set_handle(STDOUT => $writer);
2690 0           close $writer;
2691            
2692             # be safe for pseudo-fork
2693 0           my $substate = $self->deep_clone;
2694            
2695 0           $result = run_single_cmd $substate, $cmd;
2696 0           $self->reset_handles;
2697             exit $self->{exit_value}
2698 0 0         unless $pseudo_pipe;
2699             }
2700 0           close $writer;
2701            
2702 0 0         $wait_count++
2703             unless $pseudo_pipe;
2704            
2705 0           $self->set_handle(STDIN => $reader);
2706 0           close $reader;
2707            
2708 0           $cmd = $cmd->{pipe_to};
2709             }
2710 0           $result = $self->run_single_cmd($cmd);
2711            
2712 0           wait for 1..$wait_count;
2713 0           $self->reset_handles;
2714            
2715 0           return ($result, $cmd);
2716             }
2717            
2718             sub run_cmd {
2719 0     0 0   my ($self, $cmd) = @_;
2720 0 0 0       dump $cmd if $self->{dump_parse_tree} && ref $cmd;
2721            
2722 0           my $result;
2723 0           while (1) {
2724 0           ($result, $cmd) = $self->run_single_pipe($cmd);
2725 0 0         $result eq 'next' or last;
2726            
2727 0 0         $cmd->{next_cmd} or last;
2728            
2729 0 0         if (defined $cmd->{next_if}) {
2730 0 0         if ($cmd->{next_if} eq 'ok') {
2731 0 0         $self->{exit_value} or last;
2732             } else {
2733 0 0         $self->{exit_value} and last;
2734             }
2735             }
2736            
2737 0           $cmd = $cmd->{next_cmd};
2738             }
2739            
2740 0           return $result;
2741             }
2742            
2743             sub run_block {
2744 0     0 0   my ($self) = @_;
2745            
2746 0           my $result;
2747 0           while (defined $self->{pos}) {
2748 0           $result = $self->run_cmd($self->parse_cmd);
2749 0 0         if ($result eq 'return') {
    0          
    0          
2750 0           last;
2751             } elsif ($result =~ /^error/g) {
2752 0           last;
2753             } elsif ($result =~ /^exit/) {
2754 0           die 'EXIT: '.(0+$self->{exit_value});
2755             }
2756             }
2757 0           return $result;
2758             }
2759            
2760             sub run_script {
2761 0     0 0   my ($self, $attr, $lines, @arg) = @_;
2762            
2763             # perl (under Win32) or the shell (unixoid systems) unquoted the
2764             # arguments -- and the quoting was system specific in any case.
2765             #
2766             # Make the arguments look like they came from a windows command line.
2767 0 0         unless ($attr->{quoted_arguments}) {
2768             $_ = requote $_
2769 0           for @arg;
2770             }
2771            
2772             # translate %0
2773             # hope the trailing blank survives...
2774 0 0 0       @arg && defined $arg[0]
2775             and $arg[0] = $self->sys2unc($arg[0]);
2776            
2777 0           my $index = {};
2778 0 0 0       if ($lines && @$lines > 1) {
2779             # need to add exit /b to get an extra iteration in run_block,
2780             # to make calls return
2781 0           $lines = [ @$lines, '@exit /b' ];
2782            
2783             $lines->[$_] =~ /^\s*+\:\s*+(\w+)\s*+$/
2784             and $index->{uc $1} = $_+1
2785 0   0       for 0..$#$lines;
2786             }
2787            
2788 0           local $self->{args} = \@arg;
2789 0           local $self->{orig_args} = [ @arg[1..$#arg] ];
2790            
2791 0           local $self->{pos} = 0;
2792 0           local $self->{lines} = $lines;
2793 0           local $self->{index} = $index;
2794            
2795 0           local $self->{for_vars} = {};
2796 0           local $self->{call_stack} = [];
2797 0           local $self->{state_stack} = [];
2798 0           local $self->{pushd_stack} = [];
2799            
2800 0           my $result;
2801 0           eval {
2802 0           $result = $self->run_block;
2803             };
2804 0 0         if (!$@) {
    0          
2805             # $result is set
2806             } elsif ($@ =~ /^EXIT: (\d+)\s/) {
2807 0           $result = $1;
2808             } else {
2809 0           $result = "error: $@";
2810             }
2811 0           return $result;
2812             }
2813            
2814             sub run {
2815 0     0 1   my ($self, $attr, $lines, @arg) = @_;
2816            
2817 0           my $result = $self->run_script($attr, $lines, @arg);
2818             $result eq 'next' || $result eq 'return'
2819 0 0 0       and $result = $self->{exit_value};
2820 0           return $result;
2821             }
2822            
2823             1; # End of Batch::Interpreter
2824            
2825             __END__