File Coverage

blib/lib/Bat/Interpreter.pm
Criterion Covered Total %
statement 273 291 93.8
branch 90 110 81.8
condition 18 26 69.2
subroutine 26 26 100.0
pod 1 1 100.0
total 408 454 89.8


line stmt bran cond sub pod time code
1             package Bat::Interpreter;
2              
3 25     25   1864952 use utf8;
  25         423  
  25         177  
4              
5 25     25   1275 use 5.014;
  25         91  
6 25     25   13969 use Moo;
  25         287414  
  25         127  
7 25     25   50944 use Types::Standard qw(ConsumerOf);
  25         1925133  
  25         281  
8 25     25   33926 use App::BatParser 0.011;
  25         2569319  
  25         950  
9 25     25   252 use Carp;
  25         55  
  25         1632  
10 25     25   176 use Data::Dumper;
  25         58  
  25         1235  
11 25     25   11934 use Bat::Interpreter::Delegate::FileStore::LocalFileSystem;
  25         83  
  25         896  
12 25     25   11162 use Bat::Interpreter::Delegate::Executor::PartialDryRunner;
  25         75  
  25         835  
13 25     25   11102 use Bat::Interpreter::Delegate::LineLogger::Silent;
  25         80  
  25         794  
14 25     25   171 use File::Glob;
  25         55  
  25         2031  
15 25     25   176 use namespace::autoclean;
  25         54  
  25         117  
16              
17             our $VERSION = '0.025'; # VERSION
18              
19             # ABSTRACT: Pure perl interpreter for a small subset of bat/cmd files
20              
21             has 'batfilestore' => (
22             is => 'rw',
23             isa => ConsumerOf ['Bat::Interpreter::Role::FileStore'],
24             default => sub {
25             Bat::Interpreter::Delegate::FileStore::LocalFileSystem->new;
26             }
27             );
28              
29             has 'executor' => (
30             is => 'rw',
31             isa => ConsumerOf ['Bat::Interpreter::Role::Executor'],
32             default => sub {
33             Bat::Interpreter::Delegate::Executor::PartialDryRunner->new;
34             }
35             );
36              
37             has 'linelogger' => (
38             is => 'rw',
39             isa => ConsumerOf ['Bat::Interpreter::Role::LineLogger'],
40             default => sub {
41             Bat::Interpreter::Delegate::LineLogger::Silent->new;
42             }
43             );
44              
45             sub run {
46 27     27 1 26550 my $self = shift();
47 27         64 my $filename = shift();
48 27   100     235 my $external_env = shift() // \%ENV;
49              
50 27         294 my $parser = App::BatParser->new;
51              
52 27         25980 my $ensure_last_line_has_carriage_return = "\r\n";
53 27 50       150 if ( $^O eq 'MSWin32' ) {
54 0         0 $ensure_last_line_has_carriage_return = "\n";
55             }
56              
57 27         599 my $parse_tree =
58             $parser->parse( $self->batfilestore->get_contents($filename) . $ensure_last_line_has_carriage_return );
59 27 50       648967 if ($parse_tree) {
60 27         111 my $lines = $parse_tree->{'File'}{'Lines'};
61              
62 27         808 my %environment = %$external_env;
63              
64             # Index file based on labels
65             #Only for perl >= 5.020
66             #my %line_from_label = List::AllUtils::pairmap { $b->{'Label'}{'Identifier'} => $a }
67             #%{$lines}[ List::AllUtils::indexes { exists $_->{'Label'} } @$lines ];
68 27         107 my %line_from_label;
69 27         217 for ( my $i = 0; $i < scalar @$lines; $i++ ) {
70 163         305 my $line = $lines->[$i];
71 163 100       464 if ( exists $line->{'Label'} ) {
72 21         140 $line_from_label{ $line->{'Label'}{'Identifier'} } = $i;
73             }
74             }
75 27         187 $line_from_label{'EOF'} = scalar @$lines;
76 27         97 $line_from_label{'eof'} = scalar @$lines;
77 27         288 my $context = { 'ENV' => \%environment,
78             'IP' => 0,
79             'LABEL_INDEX' => \%line_from_label,
80             'current_line' => '',
81             'STACK' => [],
82             'filename' => $filename
83             };
84              
85             # Execute lines in a nonlinear fashion
86 27         134 for ( my $instruction_pointer = 0; $instruction_pointer < scalar @$lines; ) {
87 144         278 my $current_instruction = $lines->[$instruction_pointer];
88 144         253 $context->{'IP'} = $instruction_pointer;
89 144         215 my $old_ip = $instruction_pointer;
90 144         427 $self->_handle_instruction( $current_instruction, $context );
91 144         228 $instruction_pointer = $context->{'IP'};
92 144 100       326 if ( $old_ip == $instruction_pointer ) {
93 120         186 $instruction_pointer++;
94             }
95 144         345 $self->_log_line_from_context($context);
96             }
97 27         899 return $context->{'STDOUT'};
98             } else {
99 0         0 die "An error ocurred parsing the file";
100             }
101             }
102              
103             sub _handle_instruction {
104 144     144   237 my $self = shift();
105 144         209 my $current_instruction = shift();
106 144         220 my $context = shift();
107              
108 144         354 my ($type) = keys %$current_instruction;
109              
110 144 100       366 if ( $type eq 'Comment' ) {
111 4         14 $context->{'current_line'} = ":: " . $current_instruction->{'Comment'}{'Text'};
112             }
113              
114 144 100       366 if ( $type eq 'Label' ) {
115 21         95 $context->{'current_line'} = ":" . $current_instruction->{'Label'}{'Identifier'};
116             }
117              
118 144 100       371 if ( $type eq 'Statement' ) {
119 119         213 my $statement = $current_instruction->{'Statement'};
120 119         750 $self->_handle_statement( $statement, $context );
121             }
122              
123             }
124              
125             sub _handle_statement {
126 149     149   424 my $self = shift();
127 149         373 my $statement = shift();
128 149         318 my $context = shift();
129              
130 149         319 my ($type) = keys %$statement;
131              
132 149 50       368 if ( $type eq 'Command' ) {
133 149         254 my $command = $statement->{'Command'};
134 149         415 $self->_handle_command( $command, $context );
135             }
136              
137             }
138              
139             sub _handle_command {
140 149     149   228 my $self = shift();
141 149         213 my $command = shift();
142 149         217 my $context = shift();
143              
144 149 50 33     679 if ( defined $command && $command ne '' ) {
145 149         335 my ($type) = keys %$command;
146              
147 149 100       375 if ( $type eq 'SimpleCommand' ) {
148 34         100 my $command_line = $command->{'SimpleCommand'};
149 34         124 $command_line = $self->_variable_substitution( $command_line, $context );
150              
151             # Path adjustment
152 34         128 $command_line = $self->_adjust_path($command_line);
153              
154 34         103 $context->{'current_line'} .= $command_line;
155              
156 34 100       173 if ( $command_line =~ /^exit\s+\/b/i ) {
157 1         3 my $stack_frame = pop @{ $context->{'STACK'} };
  1         3  
158 1 50       4 if ( defined $stack_frame ) {
159 1         3 $context->{'IP'} = $stack_frame->{'IP'} + 1;
160             }
161             } else {
162 33         112 $self->_execute_command( $command_line, $context );
163             }
164             }
165 149 100       399 if ( $type eq 'SpecialCommand' ) {
166 115         209 my $special_command_line = $command->{'SpecialCommand'};
167 115         388 $self->_handle_special_command( $special_command_line, $context );
168             }
169             } else {
170              
171             # Empty command
172 0         0 $context->{'current_line'} .= '';
173             }
174              
175             }
176              
177             sub _handle_special_command {
178 115     115   189 my $self = shift();
179 115         170 my $special_command_line = shift();
180 115         160 my $context = shift();
181              
182 115         246 my ($type) = keys %$special_command_line;
183              
184 115 100       267 if ( $type eq 'If' ) {
185 29         82 $context->{'current_line'} .= 'IF ';
186 29         70 my $condition;
187             my $statement;
188 29 100       94 if ( exists $special_command_line->{$type}->{'NegatedCondition'} ) {
189 5         15 $context->{'current_line'} .= 'NOT ';
190 5         18 $condition = $special_command_line->{$type}->{'NegatedCondition'}->{'Condition'};
191 5         15 $statement = $special_command_line->{$type}->{'Statement'};
192 5 50       19 if ( not $self->_handle_condition( $condition, $context ) ) {
193 5         26 $self->_handle_statement( $statement, $context );
194             }
195             } else {
196 24         59 ( $condition, $statement ) = @{ $special_command_line->{'If'} }{ 'Condition', 'Statement' };
  24         98  
197 24 100       100 if ( $self->_handle_condition( $condition, $context ) ) {
198 18         93 $self->_handle_statement( $statement, $context );
199             }
200             }
201              
202             }
203              
204 115 100       271 if ( $type eq 'Goto' ) {
205 21         87 my $label = $special_command_line->{'Goto'}{'Identifier'};
206 21         77 $context->{'current_line'} .= 'GOTO ' . $label;
207 21         86 $self->_goto_label( $label, $context, 0 );
208             }
209              
210 115 100       256 if ( $type eq 'Call' ) {
211 5         18 my $token = $special_command_line->{'Call'}{'Token'};
212 5         23 $token = $self->_variable_substitution( $token, $context );
213 5         40 $token = $self->_adjust_path($token);
214 5         22 $context->{'current_line'} .= 'CALL ' . $token;
215 5 100       25 if ( $token =~ /^:/ ) {
216 2         11 $self->_goto_label( $token, $context, 1 );
217             } else {
218 3         18 ( my $first_word ) = $token =~ /\A([^\s]+)/;
219 3 50       24 if ( $first_word =~ /(\.[^.]+)$/ ) {
220 3         16 ( my $extension ) = $first_word =~ /(\.[^.]+)$/;
221 3 50 33     28 if ( $extension eq '.exe' ) {
    50          
222 0         0 $self->_execute_command( $token, $context );
223             } elsif ( $extension eq '.bat' || $extension eq '.cmd' ) {
224 3         13 $self->_log_line_from_context($context);
225 3         32 my $stdout = $self->run( $token, $context->{ENV} );
226 3 50       16 if ( !defined $context->{STDOUT} ) {
227 3         12 $context->{STDOUT} = [];
228             }
229 3 50       12 if ( defined $stdout ) {
230 0         0 push @{ $context->{STDOUT} }, @$stdout;
  0         0  
231             }
232             }
233             }
234             }
235             }
236              
237 115 100       253 if ( $type eq 'Set' ) {
238 24         66 my ( $variable, $value ) = @{ $special_command_line->{'Set'} }{ 'Variable', 'Value' };
  24         103  
239 24         101 $value = $self->_variable_substitution( $value, $context );
240 24         93 $value = $self->_adjust_path($value);
241 24         100 $context->{'current_line'} .= 'SET ' . $variable . '=' . $value;
242 24         106 $context->{ENV}{$variable} = $value;
243             }
244              
245 115 100       265 if ( $type eq 'For' ) {
246 3         7 $context->{'current_line'} .= 'FOR ';
247 3         8 my $token = $special_command_line->{'For'}{'Token'};
248              
249             # Handle only simple cases
250 3 100       33 if ( $token =~ /\s*?\/F\s*?"delims="\s*%%(?[A-Z0-9]+?)\s*?in\s*?\('(?.+)'\)/i ) {
    50          
251 1         7 my $comando = $+{'comando'};
252 1         6 my $parameter_name = $+{'variable_bucle'};
253 1         4 $comando = $self->_variable_substitution( $comando, $context );
254 1         6 $comando = $self->_adjust_path($comando);
255 1         3 $comando =~ s/%%/%/g;
256              
257 1         5 $context->{'current_line'} .= '/F "delims="' . $parameter_name . ' in ' . "'$comando' ";
258 1         4 my $salida = $self->_for_command_evaluation($comando);
259              
260 1         12 my $statement = $special_command_line->{'For'}{'Statement'};
261              
262 1         11 $context->{'PARAMETERS'}{$parameter_name} = $salida;
263              
264 1         20 $self->_handle_statement( $statement, $context );
265 1         8 delete $context->{'PARAMETERS'}{$parameter_name};
266             } elsif ( $token =~ /\s*?%%(?[A-Z0-9]+?)\s*?in\s*?(\([\d]+(?:,[^,\s]+)+\))/i ) {
267 2         8 my $statement = $special_command_line->{'For'}{'Statement'};
268 2         25 my $parameter_name = $+{'variable_bucle'};
269 2         7 my $value_list = $2;
270 2         21 $value_list =~ s/(\(|\))//g;
271 2         12 my @values = split( /,/, $value_list );
272 2         8 $context->{'current_line'} .= $token . ' do ';
273 2         8 for my $value (@values) {
274 6         17 $context->{'PARAMETERS'}->{$parameter_name} = $value;
275 6         12 $context->{'current_line'} .= "\n\t";
276 6         20 $self->_handle_statement( $statement, $context );
277 6         16 delete $context->{'PARAMETERS'}{$parameter_name};
278             }
279              
280             } else {
281 0         0 Carp::confess('FOR functionality not implemented!');
282             }
283             }
284              
285 115 100       376 if ( $type eq 'Echo' ) {
286 33         111 $context->{'current_line'} .= 'ECHO ';
287 33         90 my $echo = $special_command_line->{'Echo'};
288 33 100       109 if ( exists $echo->{'EchoModifier'} ) {
289 27         131 $context->{'current_line'} .= $echo->{'EchoModifier'};
290             } else {
291 6         17 my $message = $echo->{'Message'};
292 6         66 $message = $self->_variable_substitution( $message, $context );
293 6         25 $context->{'current_line'} .= $message;
294             }
295             }
296             }
297              
298             sub _handle_condition {
299 29     29   62 my $self = shift();
300 29         49 my $condition = shift();
301 29         56 my $context = shift();
302              
303 29         90 my ($type) = keys %$condition;
304 29 100       99 if ( $type eq 'Comparison' ) {
    50          
305             my ( $left_operand, $operator, $right_operand ) =
306 21         41 @{ $condition->{'Comparison'} }{qw(LeftOperand Operator RightOperand)};
  21         85  
307              
308 21         62 $left_operand = $self->_variable_substitution( $left_operand, $context );
309 21         68 $right_operand = $self->_variable_substitution( $right_operand, $context );
310              
311 21         96 $context->{'current_line'} .= $left_operand . ' ' . $operator . ' ' . $right_operand . ' ';
312              
313 21         104 my $uppercase_operator = uc($operator);
314 21 100 100     182 if ( $operator eq '==' || $uppercase_operator eq 'EQU' ) {
    100          
    100          
    100          
    100          
    50          
315 7         57 my $a = $left_operand =~ s/\s*(.*)\s*/$1/r;
316 7         39 my $b = $right_operand =~ s/\s*(.*)\s*/$1/r;
317 7         41 return $a eq $b;
318             } elsif ( $uppercase_operator eq 'NEQ' ) {
319 2         9 return $left_operand != $right_operand;
320             } elsif ( $uppercase_operator eq 'LSS' ) {
321 1         5 return $left_operand < $right_operand;
322             } elsif ( $uppercase_operator eq 'LEQ' ) {
323 2         13 return $left_operand <= $right_operand;
324             } elsif ( $uppercase_operator eq 'GTR' ) {
325 2         12 return $left_operand > $right_operand;
326             } elsif ( $uppercase_operator eq 'GEQ' ) {
327 7         31 return $left_operand >= $right_operand;
328              
329             } else {
330 0         0 die "Operator: $operator not implemented";
331             }
332             } elsif ( $type eq 'Exists' ) {
333 8         13 my $path = ${ $condition->{'Exists'} }{'Path'};
  8         23  
334 8         23 $path = $self->_variable_substitution( $path, $context );
335 8         24 $path = $self->_adjust_path($path);
336 8         26 $context->{'current_line'} .= 'EXIST ' . $path;
337              
338             # Glob expansion
339 8         763 my @paths = File::Glob::bsd_glob($path);
340 8         29 my $file_exists = 1;
341 8 100       28 if (@paths) {
342 6         23 for my $expanded_path (@paths) {
343 7   66     115 $file_exists = $file_exists && -e $expanded_path;
344             }
345 6         42 return $file_exists;
346             } else {
347 2         12 return 0; # If bsd_glob returns and empty array there is no such file
348             }
349             } else {
350 0         0 die "Condition type $type not implemented";
351             }
352 0         0 return 0;
353             }
354              
355             sub _variable_substitution {
356 120     120   205 my $self = shift();
357 120         189 my $string = shift();
358 120         173 my $context = shift();
359              
360 120 50       290 if ( !defined $context ) {
361 0         0 Carp::cluck "Please provide a context for variable substitution";
362             }
363              
364 120         265 my $parameters = $context->{'PARAMETERS'};
365 120 100 100     394 if ( defined $parameters && scalar keys %$parameters > 0 ) {
366              
367             my $handle_parameter_sustitution = sub {
368 7     7   24 my $parameter_name = shift();
369 7 50       18 if ( exists $parameters->{$parameter_name} ) {
370 7         43 return $parameters->{$parameter_name};
371             } else {
372 0         0 Carp::cluck "Parameter not defined: $parameter_name";
373 0         0 return '';
374             }
375 14         56 };
376 14         70 $string =~ s/%%([A-Za-z])/$handle_parameter_sustitution->($1)/eg;
  7         24  
377             }
378              
379             my $handle_variable_manipulations = sub {
380 41     41   85 my $variable_name = shift();
381 41         77 my $manipulation = shift();
382              
383 41 100 66     161 if ( defined $variable_name && $variable_name ne '' ) {
384              
385 29         85 my $result = $context->{'ENV'}{$1};
386 29 50       69 if ( defined $result ) {
387 29 100 66     95 if ( defined $manipulation && $manipulation ne '' ) {
388 5         22 $manipulation =~ s/^://;
389 5 100       34 if ( $manipulation =~ /^~(?\d+),(?\d+)$/ ) {
    100          
    50          
390 2         22 $result = substr( $result, $+{'from'}, $+{'length'} );
391             } elsif ( $manipulation =~ /^~(?-\d+),(?\d+)$/ ) {
392 1         16 $result = substr( $result, $+{'from_end'}, $+{'length'} );
393             } elsif ( $manipulation =~ /^\~(\-\d)+$/ ) {
394 2         13 $result = substr( $result, $1 );
395             } else {
396 0         0 Carp::cluck
397             "Variable manipulation not understood: $manipulation over variable: $variable_name. Returning unchanged variable: $result";
398 0         0 return $result;
399             }
400             }
401 29         117 return $result;
402             } else {
403 0         0 Carp::cluck("Variable: $variable_name not defined");
404             }
405 0         0 return '';
406             } else {
407 12         51 return '%%';
408             }
409 120         584 };
410              
411 120         448 $string =~ s/%([\w\#\$\'\(\)\*\+\,\-\.\?\@\[\]\`\{\}\~]*?)(:.+?)?%/$handle_variable_manipulations->($1, $2)/eg;
  41         119  
412              
413 120         246 $string =~ s/%%/%/g;
414              
415 120         939 return $string;
416             }
417              
418             sub _adjust_path {
419 72     72   132 my $self = shift();
420 72         121 my $path = shift();
421 72 50       305 if ( !( $^O =~ 'Win' ) ) {
422 72         166 $path =~ s/\\/\//g;
423             }
424 72         163 return $path;
425             }
426              
427             sub _execute_command {
428 33     33   82 my $self = shift();
429 33         758 $self->executor->execute_command(@_);
430             }
431              
432             sub _goto_label {
433 23     23   47 my $self = shift();
434 23         54 my $label = shift();
435 23         44 my $context = shift();
436 23         50 my $call = shift();
437 23         74 $label =~ s/^://;
438 23         54 $label =~ s/ //g;
439 23 50       84 if ( $context->{'LABEL_INDEX'}{$label} ) {
440 23 100       114 if ( $label =~ /eof/i ) {
441 4         7 my $stack_frame = pop @{ $context->{'STACK'} };
  4         11  
442 4 100       16 if ( defined $stack_frame ) {
443 1         4 $context->{'IP'} = $stack_frame->{'IP'} + 1;
444             } else {
445 3         13 $context->{'IP'} = $context->{'LABEL_INDEX'}{$label};
446             }
447             } else {
448 19 100       90 if ($call) {
449 2         4 push @{ $context->{'STACK'} }, { IP => $context->{'IP'} };
  2         8  
450             }
451 19         69 $context->{'IP'} = $context->{'LABEL_INDEX'}{$label};
452             }
453             } else {
454 0         0 die "Label: $label not indexed. Index contains: " . Dumper( $context->{'LABEL_INDEX'} );
455             }
456             }
457              
458             sub _for_command_evaluation {
459 1     1   2 my $self = shift();
460 1         3 my $comando = shift();
461 1         20 return $self->executor->execute_for_command($comando);
462             }
463              
464             sub _log_line_from_context {
465 147     147   225 my $self = shift();
466 147         260 my $context = shift();
467 147         259 my $line = $context->{'current_line'};
468 147 100 66     579 if ( defined $line && $line ne '' ) {
469 144         3014 $self->linelogger->log_line( $context->{'current_line'} );
470             }
471 147         486 $context->{'current_line'} = '';
472             }
473              
474             1;
475              
476             __END__