File Coverage

support/cmdline.yp
Criterion Covered Total %
statement 136 187 72.7
branch 62 114 54.3
condition 14 33 42.4
subroutine 30 40 75.0
pod 1 2 50.0
total 243 376 64.6


line stmt bran cond sub pod time code
1             # cmdline.yp: Parse::Yapp file for the command-line parser for App::GitFind
2             # Copyright (c) 2019 Christopher White.
3             # Copyright (c) 2019 D3 Engineering, LLC.
4             # Licensed MIT.
5              
6             #############################################################################
7             # Header
8              
9             %{
10              
11             # Imports {{{1
12              
13 4     4   60 use 5.010;
  4         10  
14 4     4   18 use strict;
  4         5  
  4         79  
15 4     4   16 use warnings;
  4         5  
  4         145  
16              
17 4     4   752 use App::GitFind::Base;
  4         5  
  4         624  
18 4     4   1104 use App::GitFind::Actions;
  4         9  
  4         19  
19 4     4   1911 use Hash::Merge;
  4         31891  
  4         462  
20              
21             # Debugging support
22             BEGIN {
23 4 50 50 4   27 if($App::GitFind::cmdline::SHOW_AST // 0) {
24 0         0 require XXX;
25 0         0 XXX->import;
26             } else { # !SHOW_AST - make YYY a passthrough
27 4     4   25 no strict 'refs';
  4         6  
  4         175  
28 4         80 *{'App::GitFind::cmdline::YYY'} = sub {
29 78 50   78   23629 return wantarray ? @_ : $_[0];
30 4         13 };
31             }
32             } #BEGIN
33              
34 4     4   19 BEGIN { YYY +{ 'YYY loaded' => 1 } }
35              
36             # }}}1
37             # Documentation {{{1
38              
39             =head1 NAME
40              
41             App::GitFind::cmdline - Command-line parser for git-find
42              
43             =head1 SYNOPSIS
44              
45             Generate the .pm file:
46              
47             yapp -m App::GitFind::cmdline -o lib/App/GitFind/cmdline.pm support/cmdline.yp
48              
49             And then:
50              
51             use App::GitFind::cmdline;
52             App::GitFind::cmdline::Parse(\@ARGV);
53              
54             For debugging output, define C<$SHOW_AST> before the C<use> statement:
55              
56             BEGIN { $App::GitFind::cmdline::SHOW_AST = 1; }
57              
58             =head1 FUNCTIONS
59              
60             =cut
61              
62             # }}}1
63             # Helpers for the parser {{{1
64              
65             # Merge any number of hashrefs together and return a hashref
66             sub _merge {
67 21     21   34 state $merger = Hash::Merge->new('RETAINMENT_PRECEDENT');
68 21         274 $merger->set_clone_behavior(false); # No cloning
69 21         179 my $retval = {};
70 21         38 for(@_) {
71 42 50       730 next unless ref eq 'HASH';
72 42         68 $retval = $merger->merge($retval, $_);
73             }
74 21         603 return $retval;
75             }
76              
77             # Check for { expr => FOO }
78             sub _is_single_expr {
79 0 0   0   0 return false unless @_ eq 1;
80 0         0 my $h = $_[0];
81 0 0       0 return false unless ref $h eq 'HASH';
82 0 0       0 return false unless keys(%$h) eq 1;
83 0 0       0 return false unless (keys %$h)[0] eq 'expr';
84 0         0 return true;
85             }
86              
87             # }}}1
88              
89             %}
90              
91             #############################################################################
92 20     20 0 91 # Token and precedence definitions
93 20 50       39  
94             %token SWITCH # flags controlling the operation of git-find
95             %token REV # a git ref or rev
96              
97             # elements of expressions
98             %token COMMA
99             %token OR
100             %token AND
101             %token NOT
102             %token LPAREN
103             %token RPAREN
104              
105             %token TEST
106             %token ACTION
107              
108             %left COMMA
109             %left OR
110             %left AND
111             %left NOT
112              
113             %%
114              
115             #############################################################################
116             # Rules
117              
118             cmdline:
119 0     0   0 { YYY +{} } # empty
120 7     7   179 | maybeexprplus { YYY $_[1] }
121             | switches_and_revs maybeexprplus
122 13     13   362 { YYY _merge($_[1], $_[2]) }
123             ;
124              
125             # Switches
126              
127 11     11   463 switches_and_revs: SWITCH { YYY +{ switches => {$_[1]=>[true]} } }
128             # arrayref so @{{switches}->{foo}} will
129             # always work. That way we can test for
130             # switches specified multiple times.
131              
132 2     2   74 | REV { YYY +{ revs => [$_[1]] } }
133             | switches_and_revs SWITCH
134 0     0   0 { YYY _merge($_[1], +{ switches => {$_[2]=>[true]} }) }
135             | switches_and_revs REV
136 8     8   262 { YYY _merge($_[1], +{ revs => [$_[2]] }) }
137             ;
138              
139             # Expressions. Since "and", "or", and "not" are keywords in Perl, the
140             # hash keys for our operators are all upper-case.
141             maybeexprplus:
142 7     7   106 { YYY +{} } # empty
143 13     13   254 | expr { YYY +{ expr => $_[1] } }
144             #{ _is_single_expr($_[1]) ? $_[1] : +{ expr => $_[1] } }
145 0     0   0 | expr switches_and_revs { YYY +{ expr => $_[1], %{$_[2]} } }
  0         0  
146             # %$_[2]: inline the switches and revs
147             ;
148              
149             # TODO once the parser is solid and the tests are written:
150             # collapse consecutive elements of the same type into flat lists.
151             expr: element
152 2     2   41 | expr COMMA expr { YYY +{ SEQ => [@_[1,3]] } }
153 1     1   20 | expr OR expr { YYY +{ OR => [@_[1,3]] } }
154             # "OR" because "or" is a Perl keyword
155              
156             # subsequent_expr is used so the parser will know when to reduce.
157             # The rule "expr expr %prec AND" was right-associative
158             # even though explicit-AND was left-associative because
159             # the parser did not start reducing until after anything that
160             # could be an expr. A subsequent_expr cannot start with
161             # a plain expr, so the parser has a reason to reduce the expr.
162             | expr subsequent_expr %prec AND
163 4     4   260 { YYY +{ AND => [@_[1,2]] } }
164 0     0   0 | expr AND expr { YYY +{ AND => [@_[1,3]] } }
165 0     0   0 | NOT expr4 { YYY +{ NOT => $_[2] } }
166 0     0   0 | LPAREN expr RPAREN { YYY $_[2] }
167             ;
168              
169             subsequent_expr: # Everything higher-precedence than AND
170             element
171 0     0   0 | NOT expr4 { YYY +{ NOT => $_[2] } }
172 0     0   0 | LPAREN expr RPAREN { YYY $_[2] }
173             ;
174              
175              
176             element: TEST
177             | ACTION
178             {
179 6 50   6   185 $_[0]->YYData->{SAW_NON_PRUNE_ACTION} = true if $_[1] ne 'prune';
180 6         31 YYY $_[1];
181             }
182 20         1225 ;
183              
184             %%
185 20         1105  
186             #############################################################################
187             # Footer
188              
189             # Helpers for the tokenizer {{{1
190              
191             # Flag a ref as invalid without using regexes.
192             # Implements https://git-scm.com/docs/git-check-ref-format as archived at
193             # https://web.archive.org/web/20190725153529/https://git-scm.com/docs/git-check-ref-format
194              
195             sub _is_ref_ok {
196 2676 50   2676   3610 my $arg = @_ ? $_[0] : $_;
197              
198 2676 50 33     5647 return false unless defined $arg and length($arg)>0;
199              
200             #1 - restrictions on slash-separated components
201 2676 100       3526 if(index($arg, '/') != -1) {
202 40 50 33     166 return false if index($arg, '/.') != -1 #internal components
      33        
      33        
203             || index($arg, '.lock/') != -1
204             || substr($arg, 0, 1) eq '.' #components at start/end
205             || substr($arg, -5) eq '.lock';
206             }
207              
208             # Ignore #2 - assume --allow-onelevel
209              
210             #3
211 2676 50       3435 return false if index($arg, '..') != -1;
212              
213             #4 - require the caller to check that
214             #5 - require the caller to check that - assume NOT --refspec-pattern
215              
216             #6 - assume NOT --normalize
217 2676 100 66     8609 return false if substr($arg, 0, 1) eq '/'
      66        
218             || substr($arg, -1) eq '/'
219             || index($arg, '//') != -1;
220              
221             # #7. Also prohibits ".", which is OK for git-find since it is
222             # fairly ambiguous between a ref/rev and a path.
223 2670 100       3358 return false if substr($arg, -1) eq '.';
224              
225             #8
226 2649 50       3077 return false if index($arg, '@{') != -1;
227              
228             #9 ('@') - ignore this one for simplicity in the rev test below.
229              
230             #10 - require the caller to check that
231              
232             # Extra: Prohibit refs that start with '--' since they are arguably
233             # ambiguous with command-line options (and I can't make them work
234             # with git anyway).
235 2649 100       5683 return false if substr($arg, 0, 2) eq '--';
236              
237 1194         3635 return true; # It's OK if we got here
238             } #_is_ref_ok()
239              
240             #use re 'debug';
241              
242             # Regex to match a rev or range of revs, i.e., something we should pass to git
243             my $_rev_regex =
244             qr`(?xi) # backtick delimiter because it doesn't occur in the regex text
245             (?&RevRange)
246              
247             (?(DEFINE)
248              
249             (?<RevRange> ^(?:
250             # :/text, :/!-text, :/!!text
251             (?::/ #(?{ print "# saw colon slash\n"; })
252             (?:
253             ![!\-](?:.+) #(?{print "# 4\n";})
254             | [^!].* #(?{print "# 5\u";})
255             )
256             )
257              
258             # :[n:]path. NOTE: we prohibit starting the path with
259             # / if there is no number, in order to disambiguate
260             # the :/ text-search cases.
261             | :\d+:(?:.+) #(?{print "# 2\n";})
262             | :[^/].* #(?{print "# 3\n";})
263              
264             # ^<rev>
265             | \^(?&Rev) #(?{print "# 6\n";})
266              
267             # rev:path
268             | (?&Rev):(?:.+) #(?{print "# 7\n";})
269              
270             # .. and ... differences, including x.., x..., x..y,
271             # and x...y. Also handles the fallthrough
272             # of revrange->rev->ref.
273             | (?&Rev)(?:\.{2,3}(?&Rev)?)?
274             #(?{print "# 8\n";})
275              
276             # ..rev and ...rev
277             | \.{2,3}(?&Rev)
278              
279             # at sign followed by braced item, and possibly
280             # preceded by a REF (not a rev). E.g.,
281             # HEAD@{1}@{1} doesn't work.
282             # refname - at sign - braced item (date, #, branch, "push")
283             | (?&Ref)?\@\{[^\}]+\}
284             #(?{print "# 9\n";})
285              
286             # git-rev-parse "Options for Objects" forms
287             | --all
288             | --(?:branches|tags|remotes)(?:=.+)?
289             | --(?:glob|exclude)=.+
290             | --disambiguate=[0-9a-f]{4,40}
291              
292             # git-rev-parse "Other Options" forms
293             | --(since|after|until|before)=.+
294              
295             )$) # End of RevRange
296              
297             (?<Rev> (?&Ref)(?&RefTrailer)* )
298             # This handles most of the cases.
299             # SHA1s, possibly abbreviated, are refs,
300             # as are git-describe outputs, whence RefTrailer*
301             # instead of RefTrailer+.
302              
303             (?<RefTrailer>
304             # For rev^[#] and rev~[#] forms
305             [~\^]\d*
306              
307             # For rev^{} forms (empty braces OK)
308             | \^\{[^\}]*\}
309              
310             # For rev^[@!] and rev^-n
311             | \^(?: \@ | ! | -\d* )
312             ) # End of RefTrailer
313              
314             (?<Ref>
315             ( \@ # '@' from git-rev-parse
316             | (?:[^\000-\037\177\ ~\^:\\?*\[.@/]
317             # git-check-ref-format #4, #5.
318             # [.@/] are handled below
319             | \.(?!\.) # . ok, but .. prohibited
320             | \@(?!\{) # @ ok, but @{ prohibited
321             | /(?!/) # / ok, but // prohibited
322              
323             )+?
324             )
325             (?(?{ _is_ref_ok($+) })|(?!))
326             # NOTE: $+ used since I couldn't get named capture groups
327             # with either %+ or %- to work
328             ) # End of <Ref>
329              
330             ) #End of (DEFINE)
331              
332             `xi; # End of qr`...` and an extra backtick to unconfuse vim-eyapp: `
333              
334             sub _is_valid_rev {
335 149 50   149   88726 my $arg = @_ ? $_[0] : $_;
336              
337 149 100 100     855 return false unless defined $arg and length($arg)>0;
338 147         3509 return scalar($arg =~ m{$_rev_regex});
339             } #_is_valid_rev()
340              
341             # Get an expression element from the array passed in $_[0].
342             my $ARGTEST_cached = App::GitFind::Actions::ARGTEST();
343             sub _consume_expression_element {
344 2710     34   3271 my $lrArgv = shift;
345 34         33 my @retval;
346              
347             #say STDERR "# Trying >>$lrArgv->[0]<<";
348             # TODO find(1) positional options, global options?
349              
350             # Regular options
351 34 100       188 if($lrArgv->[0] =~ $ARGTEST_cached) {
352             #say STDERR "# - matched";
353 20         44 my $arg = $1;
354 20         15 my %opts = %{App::GitFind::Actions::argdetails($arg)};
  20         35  
355              
356             # Save any non-parser information from the argdetails to be
357             # returned as part of the semantic value.
358 20         47 my %extras = %opts;
359 20         319 delete @extras{qw(token nparam)};
360              
361             # No-argument tests or actions
362 20 50       49 unless($opts{nparam}>0) {
363             #say STDERR "# - No parameters";
364 20         19 shift @$lrArgv;
365 20         85 return ($opts{token} => { name => $arg, %extras })
366             }
367              
368             # Consume additional arguments up to a regexp
369 0 0       0 if(ref $opts{nparam} eq 'Regexp') {
370             #say STDERR "# - parameters until $opts{nparam}";
371 0 0       0 die "Need argument(s) for --$arg" if @$lrArgv == 1;
372 0         0 my $lastarg;
373             #say STDERR "Args: ", join ' : ', @$lrArgv;
374 0         0 for(1..$#$lrArgv) {
375 0 0       0 $lastarg=$_, last if $lrArgv->[$_] =~ $opts{nparam};
376             }
377 0 0       0 die "--$arg needs an argument terminator matching $opts{nparam}"
378             unless defined $lastarg;
379              
380             # Set up to fall through to the numeric-params case
381 0         0 $opts{nparam} = $lastarg;
382             }
383              
384             # Consume additional positional arguments
385             #say STDERR "# - $opts{nparam} parameters";
386             die "Not enough parameters after --$arg (need $opts{nparam})"
387 0 0       0 unless @$lrArgv >= ($opts{nparam}+1); # +1 for $arg itself
388              
389             # Custom argument validation
390 0 0       0 if($opts{validator}) {
391 0         0 my $errmsg = $opts{validator}->(@{$lrArgv}[0..$opts{nparam}]);
  0         0  
392 0 0       0 die "--$arg argument error: $errmsg" if defined($errmsg);
393             }
394              
395             @retval = ($opts{token} => {
396             name => $arg,
397 0         0 params => [ @{$lrArgv}[1..$opts{nparam}] ],
  0         0  
398             %extras,
399             });
400 0         0 splice @$lrArgv, 0, $opts{nparam}+1;
401 0         0 return @retval;
402             }
403              
404             # Operators
405 14         19 my $arg = $lrArgv->[0];
406              
407 14 100       31 @retval = (COMMA => ',') if $arg eq ',';
408 14 100       45 @retval = (OR => '-o') if $arg =~ /^(?:-o|--o|-or|--or|\|\|)$/;
409 14 50       25 @retval = (AND => '-a') if $arg =~ /^(?:-a|--a|-and|--and|&&)$/;
410 14 50       32 @retval = (NOT => '!') if $arg =~ /^(?:-not|--not|!|\^)$/;
411 14 50       23 @retval = (LPAREN => '(') if $arg =~ /^[([]$/;
412 14 50       23 @retval = (RPAREN => ')') if $arg =~ /^[])]$/;
413              
414 14 100       24 if(@retval) {
415 3         4 shift @$lrArgv;
416 3         6 return @retval;
417             }
418              
419 11         16 return (); # Not an expression element
420             } #_consume_expression_element
421              
422             # Get a switch from the array passed in $_[0], if any.
423             # Removes the switch from the array if successful.
424             # Returns the token on success, and () on failure.
425             # TODO un-bundle switches; handle switches with args.
426             sub _consume_switch {
427 21     21   20 my $lrArgv = shift;
428 21 100       64 if($lrArgv->[0] =~ /^-([a-zA-z0-9\?])$/) { # non-bundled switch
    50          
429 11         16 shift @$lrArgv;
430 11         34 return (SWITCH => $1)
431             } elsif($lrArgv->[0] =~ /^--?(help|man|usage|version)$/) { # long switch
432 0         0 shift @$lrArgv;
433 0         0 return (SWITCH => $1);
434             }
435              
436 10         12 return ();
437             } #_consume_switch()
438              
439             # Consume a rev from the array in $_[0]
440             sub _consume_rev {
441 10     10   9 my $lrArgv = shift;
442 10         11 my $arg = $lrArgv->[0];
443 10 50       15 if(_is_valid_rev($arg)) {
444 10         17 shift @$lrArgv;
445 10         21 return (REV => $arg);
446             }
447              
448 0         0 return ();
449             } #_consume_rev()
450              
451             # }}}1
452             # Tokenizer and error-reporting routine for Parse::Yapp {{{1
453              
454             # The lexer
455             sub _next_token {
456 64     64   2720 my $parser = shift;
457 64         94 my $lrArgv = $parser->YYData->{ARGV};
458 64 100       492 return ('', undef) unless @$lrArgv; # EOF
459 47         49 my @retval; # The eventual token we will return
460              
461             # TODO? in the expression, split trailing commas into their
462             # own arguments
463              
464             # Check for '--'
465 47 100       70 if($lrArgv->[0] eq '--') {
466 8         14 $parser->YYData->{ONLY_EXPRESSIONS} = true;
467 8 100       44 return ('', undef) unless @$lrArgv > 1;
468             # We are about to shift, so return EOF if this was the last arg.
469 5         7 shift(@$lrArgv);
470             }
471              
472 44 100       58 if($parser->YYData->{HAS_DASH_DASH}) {
473             # Split-arg mode: don't look for expressions before '--', or
474             # for switches or refs after '--'.
475 16 100       101 if(!$parser->YYData->{ONLY_EXPRESSIONS}) { # Look for switches/refs
476              
477 10         48 @retval = _consume_switch($lrArgv);
478 10 100       29 return @retval if @retval;
479              
480 5         17 @retval = _consume_rev($lrArgv);
481 5 50       8 if(@retval) { # _consume_rev always gives us two elements
482 5 50       11 if($retval[1] eq ']]') {
483 0   0     0 $parser->YYData->{SAW_RR} ||= true;
484             } else {
485 5   50     8 $parser->YYData->{SAW_NON_RR} ||= true;
486             }
487 5         42 return @retval;
488             }
489              
490 0         0 die "I don't understand argument '$lrArgv->[0]' before --";
491              
492             } else { # Look for expressions
493 6         29 @retval = _consume_expression_element($lrArgv);
494 6 50       21 return @retval if @retval;
495 0         0 die "I don't understand argument '$lrArgv->[0]' after --";
496             }
497              
498             } else {
499             # Merged-arg mode: any arg could be anything
500              
501             # Check for expressions. Look for these before checking for refs so
502             # that an expression that happens to look like a ref will be considered
503             # an expression instead of a ref.
504 28         139 my @retval = _consume_expression_element($lrArgv);
505 28 100       72 return @retval if @retval;
506              
507             # Next, look for switches. These are after expression elements
508             # so that -a and -o will not be parsed as switches.
509 11         21 @retval = _consume_switch($lrArgv);
510 11 100       33 return @retval if @retval;
511              
512             # Last of all, revs.
513 5         7 @retval = _consume_rev($lrArgv);
514 5 50       11 if(@retval) { # _consume_rev always gives us two elements
515 5 50       8 if($retval[1] eq ']]') {
516 0   0     0 $parser->YYData->{SAW_RR} ||= true;
517             } else {
518 5   50     12 $parser->YYData->{SAW_NON_RR} ||= true;
519             }
520 5         41 return @retval;
521             }
522              
523 0         0 die "I don't understand argument $lrArgv->[0]";
524             }
525              
526 0         0 die "Unexpected error while processing argument $lrArgv->[0]"; # Shouldn't happen
527             } #_next_token()
528              
529             # Report an error
530             sub _report_error {
531 0     0   0 my $parser = shift;
532 0   0     0 my $got = $parser->YYCurtok || '<end of input>';
533 0         0 my $val='';
534 0 0       0 $val = ' (' . $parser->YYCurval . ')' if $parser->YYCurval;
535 0         0 die 'Syntax error: could not understand ', $got, $val, "\n";
536 0 0       0 if(ref($parser->YYExpect) eq 'ARRAY') {
537 0         0 print 'Expected one of: ', join(',', @{$parser->YYExpect}), "\n";
  0         0  
538             }
539 0         0 return;
540             } #_report_error()
541              
542             # }}}1
543             # Top-level parse function {{{1
544              
545             =head2 Parse
546              
547             Parse arguments. Usage:
548              
549             my $hrArgs = App::GitFind::cmdline::Parse(\@ARGV);
550              
551             Modifies the C<@ARGV> array.
552              
553             =cut
554              
555             sub Parse {
556 20 50   20 1 94621 my $lrArgv = shift or
557             (require Carp, Carp::croak 'Parse: Need an argument list');
558              
559 20         58 my $parser = __PACKAGE__->new;
560 20         43 my $hrData = $parser->YYData;
561              
562             # Data we use while parsing
563 20         116 $hrData->{HAS_DASH_DASH} = !!(scalar grep { $_ eq '--' } @$lrArgv);
  52         107  
564 20         30 $hrData->{ONLY_EXPRESSIONS} = false; # true once we hit '--'
565 20         23 $hrData->{ARGV} = $lrArgv;
566              
567             # Data we determine while parsing and return to the caller
568              
569             # Keep track of whether an action other than -prune has been seen.
570             # If not, -print is added automatically.
571 20         28 $hrData->{SAW_NON_PRUNE_ACTION} = false;
572              
573             # Keep track of the types of rev we've seen (]] or non-]])
574 20         29 $hrData->{SAW_RR} = false;
575 20         22 $hrData->{SAW_NON_RR} = false;
576              
577 20 50       73 my $hrRetval = $parser->YYParse(yylex => \&_next_token,
578             yyerror => \&_report_error,
579             (@_ ? (yydebug => $_[0]) : ()),
580             );
581              
582             # Add non-AST data to the retval
583 20 50       1313 $hrRetval->{saw_nonprune_action} = $hrData->{SAW_NON_PRUNE_ACTION} if $hrRetval;
584 20         42 $hrRetval->{saw_rr} = $hrData->{SAW_RR};
585 20         35 $hrRetval->{saw_non_rr} = $hrData->{SAW_NON_RR};
586 20         849 return $hrRetval;
587             } #Parse()
588              
589             # }}}1
590             # Rest of the docs {{{1
591              
592             =head1 AUTHOR
593              
594             Christopher White C<< <cxw@cpan.org> >>
595              
596             =head1 COPYRIGHT
597              
598             MIT
599              
600             =cut
601              
602             # }}}1
603              
604             # vi: set fdm=marker: #