File Coverage

blib/lib/Shell/Parser.pm
Criterion Covered Total %
statement 67 70 95.7
branch 28 36 77.7
condition 20 39 51.2
subroutine 8 9 88.8
pod 5 5 100.0
total 128 159 80.5


line stmt bran cond sub pod time code
1             package Shell::Parser;
2 5     5   67912 use strict;
  5         12  
  5         192  
3 5     5   28 use Carp;
  5         11  
  5         409  
4 5     5   4897 use Text::ParseWords;
  5         7082  
  5         338  
5              
6 5     5   30 { no strict;
  5         13  
  5         7362  
7               $VERSION = '0.04';
8             }
9              
10             =head1 NAME
11            
12             Shell::Parser - Simple shell script parser
13            
14             =head1 VERSION
15            
16             Version 0.04
17            
18             =head1 SYNOPSIS
19            
20             use Shell::Parser;
21            
22             my $parser = new Shell::Parser syntax => 'bash', handlers => {
23            
24             };
25             $parser->parse(...);
26             $parser->eof;
27            
28             =head1 DESCRIPTION
29            
30             This module implements a rudimentary shell script parser in Perl.
31             It was primarily written as a backend for C<Syntax::Highlight::Shell>,
32             in order to simplify the creation of the later.
33            
34             =head1 METHODS
35            
36             =over 4
37            
38             =item new()
39            
40             Creates and returns a new C<Shell::Parser> object.
41             Options can be provided as key/value pairs.
42            
43             B<Options>
44            
45             =over 4
46            
47             =item *
48            
49             C<handlers> - sets the parsing events handlers.
50             See L<"handlers()"> for more information.
51            
52             =item *
53            
54             C<syntax> - selects the shell syntax.
55             See L<"syntax()"> for more information.
56            
57             =back
58            
59             B<Examples>
60            
61             my $parser = new Shell::Parser syntax => 'bash',
62             handlers => { default => \&default_handler };
63            
64             =cut
65              
66             sub new {
67 4     4 1 1178     my $class = shift;
68 4         59     my $self = {
69                     handlers => {
70                         metachar => undef,
71                         keyword => undef,
72                         builtin => undef,
73                         command => undef,
74                         assign => undef,
75                         variable => undef,
76                         text => undef,
77                         comment => undef,
78                     },
79                     syntax => '',
80                 };
81                 
82 4   33     37     $class = ref($class) || $class;
83 4         12     bless $self, $class;
84                 
85             # treat given arguments
86 4         16     my %args = @_;
87 4   100     29     $args{syntax} ||= 'bourne';
88                 
89 4         17     for my $attr (keys %args) {
90 5 50       83         $self->$attr($args{$attr}) if $self->can($attr);
91                 }
92                 
93 4         17     return $self
94             }
95              
96             =item parse()
97            
98             Parse the shell code given in argument.
99            
100             B<Examples>
101            
102             $parser->parse(qq{echo "hello world"\n});
103             $parser->parse(<<'SHELL');
104             for pat; do
105             echo "greping for $pat"
106             ps aux | grep $pat
107             done
108             SHELL
109            
110             =cut
111              
112             sub parse {
113 3     3 1 32365     my $self = shift;
114                 
115             # check argument type
116 3 50       22     if(my $ref = ref $_[0]) {
117 0         0         croak "fatal: Can't deal with ref of any kind for now"
118                 }
119                 
120 3         4     my $delimiters = join '', @{ $self->{metachars} };
  3         16  
121 3         26     my @tokens = quotewords('[\s'.$delimiters.']', 'delimiters', $_[0]);
122                 
123 3         3453     while(defined(my $token = shift @tokens)) {
124 166 100       1197         next unless length $token;
125 115 50 66     482         $token .= shift @tokens if defined $tokens[0] and $tokens[0] eq $token; # e.g: '&','&' => '&&'
126                     
127 115   100     456         my $type = $self->{lookup_hash}{$token} || '';
128 115 100 50     436         $type ||= 'metachar' if index($delimiters, $token) >= 0;
129 115 100 50     255         $type ||= 'comment' if index($token, '#') == 0;
130 115 100 50     306         $type ||= 'variable' if index($token, '$') == 0;
131 115 100 50     339         $type ||= 'assign' if index($token, '=') >= 0;
132 115   100     391         $type ||= 'text';
133                     
134             # special processing
135 115 100       224         if($type eq 'comment') {
136 1   66     14             $token .= shift @tokens while @tokens and index($token, "\n") < 0;
137 1 50       12             $token =~ s/(\s*)$// and unshift @tokens, $1;
138                     }
139 115 50 66     257         if($type eq 'variable' and index($token, '(') == 1) {
140 0   0     0             $token .= shift @tokens while @tokens and index($token, ')') < 0
141                     }
142                     
143 115 50       399         &{ $self->{handlers}{$type} }($self, token => $token, type => $type)
  115         324  
144                       if defined $self->{handlers}{$type};
145                 }
146             }
147              
148             =item eof()
149            
150             Tells the parser that there is no more data.
151            
152             I<Note that this method is a no-op for now, but this may change in the future.>
153            
154             =cut
155              
156             sub eof {
157 0     0 1 0     my $self = shift;
158             }
159              
160             =item handlers()
161            
162             Assign handlers to parsing events using a hash or a hashref.
163             Available events:
164            
165             =over 4
166            
167             =item *
168            
169             C<assign> - handler for assignments: C<VARIABLE=VALUE>
170            
171             =item *
172            
173             C<builtin> - handler for shell builtin commands: C<alias>, C<jobs>, C<read>...
174            
175             =item *
176            
177             C<command> - handler for external commands (I<not implemented>)
178            
179             =item *
180            
181             C<comment> - handler for comments: C<# an impressive comment>
182            
183             =item *
184            
185             C<keyword> - handler for shell reserved words: C<for>, C<if>, C<case>...
186            
187             =item *
188            
189             C<metachar> - handler for shell metacharacters: C<;>, C<&>, C<|>...
190            
191             =item *
192            
193             C<variable> - handler for variable expansion: C<$VARIABLE>
194            
195             =item *
196            
197             C<text> - handler for anything else
198            
199             =back
200            
201             There is also a C<default> handler, which will be used for any handler
202             which has not been explicitely defined.
203            
204             B<Examples>
205            
206             # set the default event handler
207             $parser->handlers(default => \&default_handler);
208            
209             # set the 'builtin' and 'keywords' events handlers
210             $parser->handlers({ builtin => \&handle_internals, keywords => \&handle_internals });
211            
212             See also L<"Handlers"> for more information on how event handlers receive
213             their data in argument.
214            
215             =cut
216              
217             sub handlers {
218 6     6 1 4535     my $self = shift;
219 6 100       33     my %handlers = ref $_[0] ? %{$_[0]} : @_;
  2         12  
220                 
221 6         9     my $default = undef;
222 6 100       23     $default = delete $handlers{default} if $handlers{default};
223                 
224 6         19     for my $handler (keys %handlers) {
225 8 50 0     19         carp "error: No such handler: $handler" and next unless exists $self->{handlers}{$handler};
226 8   33     24         $self->{handlers}{$handler} = $handlers{$handler} || $default;
227                 }
228                 
229 6         11     for my $handler (keys %{$self->{handlers}}) {
  6         22  
230 48   66     144         $self->{handlers}{$handler} ||= $default
231                 }
232             }
233              
234             =item syntax()
235            
236             Selects the shell syntax. Use one of:
237            
238             =over 4
239            
240             =item *
241            
242             C<bourne> - the standard Bourne shell
243            
244             =item *
245            
246             C<csh> - the C shell
247            
248             =item *
249            
250             C<tcsh> - the TENEX C shell
251            
252             =item *
253            
254             C<korn88> - the Korn shell, 1988 version
255            
256             =item *
257            
258             C<korn93> - the Korn shell 1993 version
259            
260             =item *
261            
262             C<bash> - GNU Bourne Again SHell
263            
264             =item *
265            
266             C<zsh> - the Z shell
267            
268             =back
269            
270             Returns the current syntax when called with no argument, or the previous
271             syntax when affecting a new one.
272            
273             =cut
274              
275             # Note:
276             # - keywords are the "reserved words" in *sh man pages
277             # - builtins are the "builtin commands" in *sh man pages
278              
279             my %shell_syntaxes = (
280                 bourne => {
281                     name => 'Bourne shell',
282                     metachars => [ qw{ ; & ( ) | < > } ],
283                     keywords => [ qw(
284             ! { } case do done elif else esac for if fi in then until while
285             ) ],
286                     builtins => [ qw(
287             alias bg break cd command continue eval exec exit export fc fg
288             getopts hash jobid jobs local pwd read readonly return select
289             set setvar shift trap ulimit umask unalias unset wait
290             ) ],
291                 },
292                 
293                 csh => {
294                     name => 'C-shell',
295                     metachars => [ qw{ ; & ( ) | < > } ],
296                     keywords => [ qw(
297             breaksw case default else end endif endsw foreach if switch then while
298             ) ],
299                     builtins => [ qw(
300             % @ alias alloc bg break cd chdir continue dirs echo eval exec
301             exit fg glob goto hashstat history jobs kill limit login logout
302             nice nohup notify onintr popd pushd rehash repeat set setenv
303             shift source stop suspend time umask unalias unhash unlimit unset
304             unsetenv wait which
305             ) ],
306                 },
307                 
308                 tcsh => {
309                     name => 'TENEX C-shell',
310                     metachars => [ qw{ ; & ( ) | < > } ],
311                     keywords => [ qw(
312             breaksw case default else end endif endsw foreach if switch then while
313             ) ],
314                     builtins => [ qw(
315             : % @ alias alloc bg bindkey break builtins bye cd chdir complete
316             continue dirs echo echotc eval exec exit fg filetest getspath
317             getxvers glob goto hashstat history hup inlib jobs kill limit log
318             login logout ls-F migrate newgrp nice nohup notify onintr popd
319             printenv pushd rehash repeat rootnode sched set setenv setpath
320             setspath settc setty setxvers shift source stop suspend telltc
321             time umask unalias uncomplete unhash universe unlimit unset
322             unsetenv ver wait warp watchlog where which
323             ) ],
324                 },
325                 
326                 korn88 => {
327                     name => 'Korn shell 88',
328                     metachars => [ qw{ ; & ( ) | < > } ],
329                     keywords => [ qw(
330             { } [[ ]] case do done elif else esac fi for function if select
331             time then until while
332             ) ],
333                     builtins => [ qw(
334             : . alias bg break continue cd command echo eval exec exit export
335             fc fg getopts hash jobs kill let login newgrp print pwd read
336             readonly return set shift stop suspend test times trap type
337             typeset ulimit umask unalias unset wait whence
338             ) ],
339                 },
340                 
341                 korn93 => {
342                     name => 'Korn shell 93',
343                     metachars => [ qw{ ; & ( ) | < > } ],
344                     keywords => [ qw(
345             ! { } [[ ]] case do done elif else esac fi for function if select
346             then time until while
347             ) ],
348                     builtins => [ qw(
349             : . alias bg break builtin cd command continue disown echo eval
350             exec exit export false fg getconf getopts hist jobs kill let newgrp
351             print printf string pwd read readonly return set shift sleep trap
352             true typeset ulimit umask unalias unset wait whence
353             ) ],
354                 },
355                 
356                 bash => {
357                     name => 'Bourne Again SHell',
358                     metachars => [ qw{ ; & ( ) | < > } ],
359                     keywords => [ qw(
360             ! { } [[ ]] case do done elif else esac fi for function if in
361             select then time until while
362             ) ],
363                     builtins => [ qw(
364             : . alias bg bind break builtin cd command compgen complete
365             continue declare dirs disown echo enable eval exec exit export
366             fc fg getopts hash help history jobs kill let local logout popd
367             printf pushd pwd read readonly return set shift shopt source
368             suspend test times trap type typeset ulimit umask unalias unset
369             wait
370             ) ],
371                 },
372                 
373                 zsh => {
374                     name => 'Z shell',
375                     metachars => [ qw{ ; & ( ) | < > } ],
376                     keywords => [ qw(
377             ! { } [[ case coproc do done elif else end esac fi for foreach
378             function if in nocorrect repeat select then time until while
379             ) ],
380                     builtins => [ qw(
381             : . [ alias autoload bg bindkey break builtin bye cap cd chdir
382             clone command comparguments compcall compctl compdescribe compfiles
383             compgroups compquote comptags comptry compvalues continue declare
384             dirs disable disown echo echotc echoti emulate enable eval exec
385             exit export false fc fg float functions getcap getln getopts hash
386             history integer jobs kill let limit local log logout noglob popd
387             print pushd pushln pwd r read readonly rehash return sched set
388             setcap setopt shift source stat suspend test times trap true ttyctl
389             type typeset ulimit umask unalias unfunction unhash unlimit unset
390             unsetopt vared wait whence where which zcompile zformat zftp zle
391             zmodload zparseopts zprof zpty zregexparse zstyle
392             ) ],
393                 },
394             );
395              
396             sub syntax {
397 24     24 1 7114     my $self = shift;
398 24         52     my $old = $self->{syntax};
399 24 100       68     $self->{syntax} = $_[0] if $_[0];
400 24         34     my $syntax = $self->{syntax};
401                 
402 24 100       72     if($syntax ne $old) {
403 10 50 0     45         carp "error: Unknown syntax '$syntax'" and return unless exists $shell_syntaxes{$syntax};
404                     
405             # (re)initialize the lookup hash when the syntax given in argument
406             # is different from the syntax we already had
407 10         27         $self->{lookup_hash} = {};
408 10         90         $self->{metachars} = [ @{ $shell_syntaxes{$syntax}{metachars} } ];
  10         53  
409                     
410 10         23         for my $type (qw(keyword builtin)) {
411 20         29             my @words = @{ $shell_syntaxes{$syntax}{"${type}s"} };
  20         155  
412 20         49             @{ $self->{lookup_hash} }{@words} = ($type) x scalar @words;
  20         1263  
413                     }
414                 }
415                 
416 24         94     return $self->{syntax}
417             }
418              
419             =back
420            
421             =head1 HANDLERS
422            
423             During parsing, the functions defined as handlers for the corresponding
424             events will be called with the following arguments:
425            
426             =over 4
427            
428             =item *
429            
430             a reference to the current C<Shell::Parser> object
431            
432             =item *
433            
434             a hash with the following keys:
435            
436             =over 4
437            
438             =item *
439            
440             C<token> - the actual shell token
441            
442             =item *
443            
444             C<type> - the type of the token
445            
446             =back
447            
448             =back
449            
450             Therefore, a typical handler function will begin with something like this:
451            
452             sub my_handler {
453             my $self = shift;
454             my %args = @_;
455            
456             # do stuff
457             # ...
458             }
459            
460             =head1 EXAMPLE
461            
462             Here is an example that shows how the tokens are given to the events
463             handlers. It uses the script F<eg/parsedump.pl>:
464            
465             #!/usr/bin/perl
466             use strict;
467             use Shell::Parser;
468            
469             my $parser = new Shell::Parser handlers => { default => \&dumpnode };
470             $parser->parse(join '', <>);
471            
472             sub dumpnode {
473             my $self = shift;
474             my %args = @_;
475             print "$args{type}: <$args{token}>\n"
476             }
477            
478             Running this Perl script with the following shell script in argument:
479            
480             #!/bin/sh
481             if [ "$text" != "" ]; then grep "$text" file.txt; fi
482            
483             will produce the following trace:
484            
485             comment: <#!/bin/sh>
486             text: <
487             >
488             keyword: <if>
489             text: < >
490             text: <[>
491             text: < >
492             text: <"$text">
493             text: < >
494             assign: <!=>
495             text: < >
496             text: <"">
497             text: < >
498             text: <]>
499             metachar: <;>
500             text: < >
501             keyword: <then>
502             text: < >
503             text: <grep>
504             text: < >
505             text: <"$text">
506             text: < >
507             text: <file.txt>
508             metachar: <;>
509             text: < >
510             keyword: <fi>
511             text: <
512             >
513            
514             =head1 DIAGNOSTICS
515            
516             =over 4
517            
518             =item Can't deal with ref of any kind for now
519            
520             B<(F)> You gave a reference to C<parse()>, which is not handled at
521             this time.
522            
523             =item No such handler: %s
524            
525             B<(E)> You gave an unknown handler name. Please check L<"handlers()">
526             for the available handlers.
527            
528             =item Unknown syntax '%s'
529            
530             B<(E)> You gave an unknown syntax. Please check L<"syntax()"> for the
531             available syntaxes.
532            
533             =back
534            
535             =head1 CAVEATS
536            
537             =over 4
538            
539             =item *
540            
541             Running C<Shell::Parser> with the C<-W> flag gives many warnings, but most
542             come from C<L<Text::ParseWords>>.
543            
544             =item *
545            
546             Comments curently contains the newline character that terminate them.
547             This is not very intuituive and will be corrected in later versions.
548            
549             =item *
550            
551             The C<command> event is currently unimplemented.
552            
553             =item *
554            
555             Here-documents are currently not parsed.
556            
557             =back
558            
559             =head1 AUTHOR
560            
561             SEeacute>bastien Aperghis-Tramoni, E<lt>sebastien@aperghis.netE<gt>
562            
563             =head1 BUGS
564            
565             Please report any bugs or feature requests to
566             C<bug-shell-parser@rt.cpan.org>, or through the web interface at
567             L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Shell-Parser>.
568             I will be notified, and then you'll automatically be notified of
569             progress on your bug as I make changes.
570            
571             =head1 COPYRIGHT & LICENSE
572            
573             Copyright 2004 SE<eacute>bastien Aperghis-Tramoni, All Rights Reserved.
574            
575             This program is free software; you can redistribute it and/or modify it
576             under the same terms as Perl itself.
577            
578             =cut
579              
580             1; # End of Shell::Parser
581