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