File Coverage

blib/lib/Bat/Interpreter.pm
Criterion Covered Total %
statement 264 282 93.6
branch 88 108 81.4
condition 16 23 69.5
subroutine 25 25 100.0
pod 1 1 100.0
total 394 439 89.7


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