File Coverage

blib/lib/OptArgs2.pm
Criterion Covered Total %
statement 317 441 71.8
branch 92 184 50.0
condition 47 135 34.8
subroutine 38 48 79.1
pod 7 22 31.8
total 501 830 60.3


line stmt bran cond sub pod time code
1 3     3   385024 use strict;
  3         14  
  3         73  
2 3     3   12 use warnings;
  3         3  
  3         84  
3              
4             package OptArgs2;
5 3     3   457 use Encode qw/decode/;
  3         8613  
  3         166  
6             use Exporter::Tidy
7 3         20 default => [qw/class_optargs cmd optargs subcmd/],
8 3     3   2623 other => [qw/usage cols rows/];
  3         1300  
9              
10             our $VERSION = '2.0.0_4';
11             our @CARP_NOT = (
12             qw/
13             OptArgs2
14             OptArgs2::Arg
15             OptArgs2::Cmd
16             OptArgs2::CmdBase
17             OptArgs2::Opt
18             OptArgs2::OptArgBase
19             OptArgs2::SubCmd
20             /
21             );
22              
23             # constants
24             sub STYLE_USAGE() { 'Usage' } # default
25             sub STYLE_HELP() { 'Help' }
26             sub STYLE_HELPTREE() { 'HelpTree' }
27             sub STYLE_HELPSUMMARY() { 'HelpSummary' }
28              
29             my %COMMAND;
30              
31             my @chars;
32              
33             sub _chars {
34 0 0   0   0 if ( $^O eq 'MSWin32' ) {
35 0         0 require Win32::Console;
36 0         0 @chars = Win32::Console->new()->Size();
37             }
38             else {
39 0         0 require Term::Size::Perl;
40 0         0 @chars = Term::Size::Perl::chars();
41             }
42 0         0 \@chars;
43             }
44              
45             sub cols {
46 0   0 0 1 0 $chars[0] // _chars()->[0];
47             }
48              
49             sub rows {
50 0   0 0 1 0 $chars[1] // _chars()->[1];
51             }
52              
53             my %error_types = (
54             CmdExists => undef,
55             CmdNotFound => undef,
56             Conflict => undef,
57             InvalidIsa => undef,
58             ParentCmdNotFound => undef,
59             SubCmdExists => undef,
60             UndefOptArg => undef,
61             Usage => undef,
62             );
63              
64             sub throw_error {
65 1     1 0 5 require Carp;
66              
67 1         1 my $proto = shift;
68 1   33     4 my $type = shift // Carp::croak( 'Usage', 'error($TYPE, [$msg])' );
69 1         2 my $pkg = 'OptArgs2::Error::' . $type;
70 1   33     3 my $msg = shift // "($pkg)";
71 1 50       2 $msg = sprintf( $msg, @_ ) if @_;
72              
73             Carp::croak( 'Usage', "unknown error type: $type" )
74 1 50       4 unless exists $error_types{$type};
75              
76 1         227 $msg .= ' ' . Carp::longmess('');
77              
78 3     3   1122 no strict 'refs';
  3         4  
  3         640  
79 1         27 *{ $pkg . '::ISA' } = ['OptArgs2::Status'];
  1         13  
80              
81 1         7 die bless \$msg, $pkg;
82             }
83              
84             my %usage_types = (
85             ArgRequired => undef,
86             Help => undef,
87             HelpSummary => undef,
88             HelpTree => undef,
89             OptRequired => undef,
90             OptUnknown => undef,
91             SubCmdRequired => undef,
92             SubCmdUnknown => undef,
93             UnexpectedOptArg => undef,
94             );
95              
96             sub throw_usage {
97 4     4 0 6 my $proto = shift;
98 4   33     10 my $type = shift // $proto->error( 'Usage', 'usage($TYPE, $str)' );
99 4   33     9 my $str = shift // $proto->error( 'Usage', 'usage($type, $STR)' );
100 4         22 my $pkg = 'OptArgs2::Usage::' . $type;
101              
102             $proto->error( 'Usage', "unknown usage reason: $type" )
103 4 50       43 unless exists $usage_types{$type};
104              
105 4 50       38 if ( -t STDERR ) {
106 0         0 my $lines = scalar( split /\n/, $str );
107 0 0       0 $lines++ if $str =~ m/\n\z/;
108              
109 0 0       0 if ( $lines >= OptArgs2::rows() ) {
110 0         0 require OptArgs2::Pager;
111 0         0 my $pager = OptArgs2::Pager->new( auto => 0 );
112 0         0 local *STDERR = $pager->fh;
113              
114 3     3   21 no strict 'refs';
  3         5  
  3         219  
115 0         0 *{ $pkg . '::ISA' } = ['OptArgs2::Status'];
  0         0  
116 0         0 die bless \$str, $pkg;
117             }
118             }
119              
120 3     3   17 no strict 'refs';
  3         18  
  3         1668  
121 4         11 *{ $pkg . '::ISA' } = ['OptArgs2::Status'];
  4         83  
122 4         38 die bless \$str, $pkg;
123             }
124              
125             sub class_optargs {
126 11   33 11 1 8173 my $class = shift
127             || OptArgs2->throw_error( 'Usage', 'class_optargs($CMD,[@argv])' );
128              
129 11   33     32 my $cmd = $COMMAND{$class}
130             || OptArgs2->throw_error( 'CmdNotFound',
131             'command class not found: ' . $class );
132              
133 11         22 my @source = @_;
134              
135 11 100 100     34 if ( !@_ and @ARGV ) {
136             my $CODESET =
137 1         2 eval { require I18N::Langinfo; I18N::Langinfo::CODESET() };
  1         404  
  1         522  
138              
139 1 50       4 if ($CODESET) {
140 1         4 my $codeset = I18N::Langinfo::langinfo($CODESET);
141 1         6 $_ = decode( $codeset, $_ ) for @ARGV;
142             }
143              
144 1         552 @source = @ARGV;
145             }
146              
147 11         32 $cmd->parse(@source);
148             }
149              
150             sub cmd {
151 6   33 6 1 6286 my $class = shift || OptArgs2->throw_error( 'Usage', 'cmd($CLASS,@args)' );
152              
153             OptArgs2->throw_error( 'CmdExists', "command already defined: $class" )
154 6 50       17 if exists $COMMAND{$class};
155              
156 6         40 $COMMAND{$class} = OptArgs2::Cmd->new( class => $class, @_ );
157             }
158              
159             sub optargs {
160 1     1 1 5067 my $class = caller;
161 1         4 cmd( $class, @_ );
162 1         3 ( class_optargs($class) )[1];
163             }
164              
165             sub subcmd {
166 6   33 6 1 137 my $class =
167             shift || OptArgs2->throw_error( 'Usage', 'subcmd($CLASS,%%args)' );
168              
169             OptArgs2->throw_error( 'SubCmdExists',
170             "subcommand already defined: $class" )
171 6 50       26 if exists $COMMAND{$class};
172              
173 6 100       50 OptArgs2->throw_error( 'ParentCmdNotFound',
174             "no '::' in class '$class' - must have a parent" )
175             unless $class =~ m/(.+)::(.+)/;
176              
177 5         21 my ( $parent_class, $name ) = ( $1, $2 );
178              
179             OptArgs2->throw_error( 'ParentCmdNotFound',
180             "parent class not found: " . $parent_class )
181 5 50       12 unless exists $COMMAND{$parent_class};
182              
183 5         16 $COMMAND{$class} = $COMMAND{$parent_class}->add_cmd(
184             name => $name,
185             @_
186             );
187             }
188              
189             sub usage {
190 0   0 0 1 0 my $class =
191             shift || OptArgs2->throw_error( 'Usage', 'usage($CLASS,[$style])' );
192 0         0 my $style = shift;
193              
194             OptArgs2->throw_error( 'CmdNotFound', "command not found: $class" )
195 0 0       0 unless exists $COMMAND{$class};
196              
197 0         0 return $COMMAND{$class}->usage_string($style);
198             }
199              
200             package OptArgs2::Status {
201             use overload
202 5     5   149 bool => sub { 1 },
203 0     0   0 '""' => sub { ${ $_[0] } },
  0         0  
204 3     3   998 fallback => 1;
  3         817  
  3         31  
205             }
206              
207             package OptArgs2::CODEREF {
208             our @CARP_NOT = @OptArgs2::CARP_NOT;
209              
210             sub TIESCALAR {
211 0     0   0 my $class = shift;
212 0 0       0 ( 3 == @_ )
213             or Optargs2->throw_error( 'Usage', 'args: optargs,name,sub' );
214 0         0 return bless [@_], $class;
215             }
216              
217             sub FETCH {
218 0     0   0 my $self = shift;
219 0         0 my ( $optargs, $name, $sub ) = @$self;
220 0         0 untie $optargs->{$name};
221 0         0 $optargs->{$name} = $sub->($optargs);
222             }
223             }
224              
225             package OptArgs2::OptArgBase {
226             use OptArgs2::OptArgBase_CI
227 3         31 abstract => 1,
228             has => {
229             comment => { required => 1, },
230             default => {},
231             getopt => {},
232             isa => { required => 1, },
233             isa_name => { is => 'rw', },
234             name => { required => 1, },
235             required => {},
236             show_default => {},
237             },
238 3     3   1715 ;
  3         6  
239              
240             our @CARP_NOT = @OptArgs2::CARP_NOT;
241             our %isa2name = (
242             'ArrayRef' => 'Str',
243             'Bool' => '',
244             'Counter' => '',
245             'Flag' => '',
246             'HashRef' => 'Str',
247             'Int' => 'Int',
248             'Num' => 'Num',
249             'Str' => 'Str',
250             'SubCmd' => 'Str',
251             );
252              
253             }
254              
255             package OptArgs2::Arg {
256             use OptArgs2::Arg_CI
257 3         20 isa => 'OptArgs2::OptArgBase',
258             has => {
259             cmd => { is => 'rw', weaken => 1, },
260             fallthru => {},
261             greedy => {},
262 3     3   1020 };
  3         12  
263              
264             our @CARP_NOT = @OptArgs2::CARP_NOT;
265             my %arg2getopt = (
266             'Str' => '=s',
267             'Int' => '=i',
268             'Num' => '=f',
269             'ArrayRef' => '=s@',
270             'HashRef' => '=s%',
271             'SubCmd' => '=s',
272             );
273              
274             sub BUILD {
275 6     6 0 10 my $self = shift;
276              
277 6 50 66     20 OptArgs2->throw_error( 'Conflict',
278             q{'default' and 'required' conflict} )
279             if $self->required and defined $self->default;
280             }
281              
282             sub name_alias_type_comment {
283 4     4 0 13 my $self = shift;
284 4         6 my $value = shift;
285              
286 4         6 my $deftype = '';
287 4 50       7 if ( defined $value ) {
288 0         0 $deftype = '[' . $value . ']';
289             }
290             else {
291             $deftype = $self->isa_name
292 4   33     11 // $OptArgs2::OptArgBase::isa2name{ $self->isa }
      33        
293             // OptArgs2->throw_error( 'InvalidIsa',
294             'invalid isa type: ' . $self->isa );
295             }
296              
297 4         11 my $comment = $self->comment;
298 4 100       8 if ( $self->required ) {
299 3 50       12 $comment .= ' ' if length $comment;
300 3         4 $comment .= '*required*';
301             }
302              
303 4         10 return $self->name, '', $deftype, $comment;
304             }
305              
306             }
307              
308             package OptArgs2::Opt {
309             use OptArgs2::Opt_CI
310 3         26 isa => 'OptArgs2::OptArgBase',
311             has => {
312             alias => {},
313             hidden => {},
314             trigger => {},
315 3     3   1052 };
  3         6  
316              
317             our @CARP_NOT = @OptArgs2::CARP_NOT;
318              
319             my %isa2getopt = (
320             'ArrayRef' => '=s@',
321             'Bool' => '!',
322             'Counter' => '+',
323             'Flag' => '!',
324             'HashRef' => '=s%',
325             'Int' => '=i',
326             'Num' => '=f',
327             'Str' => '=s',
328             );
329              
330             sub new_from {
331 8     8 0 15 my $proto = shift;
332 8         21 my $ref = {@_};
333              
334 8 50       19 if ( exists $ref->{ishelp} ) {
335 0         0 Carp::carp( '"ishelp" is deprecated in favour of '
336             . '"isa => OptArgs2::STYLE_HELP"' );
337 0         0 delete $ref->{ishelp};
338 0   0     0 $ref->{isa} //= OptArgs2::STYLE_HELP;
339             }
340              
341 8 100       40 if ( $ref->{isa} =~ m/^Help/ ) { # one of the STYLE_HELPs
342 6         13 my $style = $ref->{isa};
343 6         9 my $name = $style;
344 6         18 $name =~ s/([a-z])([A-Z])/$1-$2/g;
345 6         10 $ref->{isa} = 'Counter';
346 6   33     30 $ref->{name} //= lc $name;
347 6   33     34 $ref->{alias} //= lc substr $ref->{name}, 0, 1;
348 6   33     43 $ref->{comment} //= "print a $style message and exit";
349             $ref->{trigger} //= sub {
350 0     0   0 my $cmd = shift;
351 0         0 my $val = shift;
352              
353 0 0       0 if ( $val == 1 ) {
    0          
354 0         0 OptArgs2->throw_usage( OptArgs2::STYLE_HELP,
355             $cmd->usage_string(OptArgs2::STYLE_HELP) );
356             }
357             elsif ( $val == 2 ) {
358 0         0 OptArgs2->throw_usage( OptArgs2::STYLE_HELPTREE,
359             $cmd->usage_string(OptArgs2::STYLE_HELPTREE) );
360             }
361             else {
362 0         0 OptArgs2->throw_usage(
363             'UnexpectedOptArg',
364             $cmd->usage_string(
365             OptArgs2::STYLE_USAGE,
366             qq{"--$ref->{name}" used too many times}
367             )
368             );
369             }
370 6   50     50 };
371             }
372              
373 8 50       25 if ( !exists $isa2getopt{ $ref->{isa} } ) {
374             return OptArgs2->throw_error( 'InvalidIsa',
375             'invalid isa "%s" for opt "%s"',
376 0         0 $ref->{isa}, $ref->{name} );
377             }
378              
379 8         15 $ref->{getopt} = $ref->{name};
380 8 50       21 if ( $ref->{name} =~ m/_/ ) {
381 0         0 ( my $x = $ref->{name} ) =~ s/_/-/g;
382 0         0 $ref->{getopt} .= '|' . $x;
383             }
384 8 100       20 $ref->{getopt} .= '|' . $ref->{alias} if $ref->{alias};
385 8         17 $ref->{getopt} .= $isa2getopt{ $ref->{isa} };
386              
387 8         39 return $proto->new(%$ref);
388             }
389              
390             sub name_alias_type_comment {
391 4     4 0 7 my $self = shift;
392 4         4 my $value = shift;
393              
394 4         10 ( my $opt = $self->name ) =~ s/_/-/g;
395 4 50       12 if ( $self->isa eq 'Bool' ) {
396 0 0       0 if ($value) {
    0          
397 0         0 $opt = 'no-' . $opt;
398             }
399             elsif ( not defined $value ) {
400 0         0 $opt = '[no-]' . $opt;
401             }
402             }
403 4         8 $opt = '--' . $opt;
404              
405 4   50     11 my $alias = $self->alias // '';
406 4 50       10 if ( length $alias ) {
407 4         5 $opt .= ',';
408 4         12 $alias = '-' . $alias;
409             }
410              
411 4         10 my $deftype = '';
412 4 50       7 if ( defined $value ) {
413 0 0       0 if ( $self->isa eq 'Flag' ) {
    0          
    0          
414 0         0 $deftype = '(set)';
415             }
416             elsif ( $self->isa eq 'Bool' ) {
417 0 0       0 $deftype = '(' . ( $value ? 'true' : 'false' ) . ')';
418             }
419             elsif ( $self->isa eq 'Counter' ) {
420 0         0 $deftype = '(' . $value . ')';
421             }
422             else {
423 0         0 $deftype = '[' . $value . ']';
424             }
425             }
426             else {
427             $deftype = $self->isa_name
428 4   33     12 // $OptArgs2::OptArgBase::isa2name{ $self->isa }
      33        
429             // OptArgs2->throw_error( 'InvalidIsa',
430             'invalid isa type: ' . $self->isa );
431             }
432              
433 4         11 my $comment = $self->comment;
434 4 50       8 if ( $self->required ) {
435 0 0       0 $comment .= ' ' if length $comment;
436 0         0 $comment .= '*required*';
437             }
438              
439 4         13 return $opt, $alias, $deftype, $comment;
440             }
441              
442             }
443              
444             package OptArgs2::CmdBase {
445             use overload
446 30     30   80 bool => sub { 1 },
447 0     0   0 '""' => sub { shift->class },
448 3     3   19 fallback => 1;
  3         7  
  3         30  
449 3     3   2187 use Getopt::Long qw/GetOptionsFromArray/;
  3         26517  
  3         9  
450 3     3   458 use List::Util qw/max/;
  3         6  
  3         448  
451             use OptArgs2::CmdBase_CI
452             abstract => 1,
453             has => {
454             abbrev => { is => 'rw', },
455 11         43 args => { default => sub { [] }, },
456             comment => { required => 1, },
457             hidden => {},
458             no_help => { default => 0 },
459             optargs => {
460             is => 'rw',
461 0         0 default => sub { [] }
462             },
463 11         37 opts => { default => sub { [] }, },
464             parent => { weaken => 1, },
465             _opts => {
466 0         0 default => sub { {} }
467             },
468             _args => {
469 0         0 default => sub { {} }
470             },
471             _subcmds => {
472 5         16 default => sub { {} }
473             },
474             show_default => { default => 0, },
475 8         70 show_color => { default => sub { -t STDERR }, },
476 6         20 subcmds => { default => sub { [] }, },
477 3         64 _values => { is => 'rw' },
478             },
479 3     3   1030 ;
  3         8  
480              
481             our @CARP_NOT = @OptArgs2::CARP_NOT;
482              
483             sub BUILD {
484 11     11 0 18 my $self = shift;
485              
486 11         19 while ( my ( $name, $args ) = splice @{ $self->optargs }, 0, 2 ) {
  19         48  
487 8 100       29 if ( $args->{isa} =~ s/^--// ) {
488 2         9 $self->add_opt(
489             name => $name,
490             %$args,
491             );
492             }
493             else {
494 6         37 $self->add_arg(
495             name => $name,
496             %$args,
497             );
498             }
499             }
500             }
501              
502             sub add_arg {
503 6     6 0 11 my $self = shift;
504 6         22 my $arg = OptArgs2::Arg->new(
505             cmd => $self,
506             show_default => $self->show_default,
507             @_,
508             );
509              
510 6         9 push( @{ $self->args }, $arg );
  6         33  
511 6         14 $arg;
512             }
513              
514             sub add_cmd {
515 5     5 0 10 my $self = shift;
516 5         20 my $subcmd = OptArgs2::SubCmd->new(
517             abbrev => $self->abbrev,
518             show_default => $self->show_default,
519             @_,
520             parent => $self,
521             );
522              
523 5 50       23 Carp::croak "cmd exists" if exists $self->_subcmds->{ $subcmd->name };
524              
525 5         12 $self->_subcmds->{ $subcmd->name } = $subcmd;
526 5         7 push( @{ $self->subcmds }, $subcmd );
  5         16  
527              
528 5         16 return $subcmd;
529             }
530              
531             sub add_opt {
532 8     8 0 13 my $self = shift;
533 8         16 my $opt = OptArgs2::Opt->new_from(
534             show_default => $self->show_default,
535             @_,
536             );
537              
538 8         14 push( @{ $self->opts }, $opt );
  8         33  
539 8         23 $opt;
540             }
541              
542             sub parents {
543 23     23 0 31 my $self = shift;
544 23 100       51 return unless $self->parent;
545 4         10 return ( $self->parent->parents, $self->parent );
546             }
547              
548             sub parse {
549 11     11 0 16 my $self = shift;
550 11         15 my $source = \@_;
551              
552             map {
553 11 50       24 OptArgs2->throw_error( 'UndefOptArg',
  8         30  
554             'optargs argument undefined!' )
555             if !defined $_
556             } @$source;
557              
558 11         19 my $source_hash = { map { %$_ } grep { ref $_ eq 'HASH' } @$source };
  0         0  
  8         20  
559 11         19 $source = [ grep { ref $_ ne 'HASH' } @$source ];
  8         15  
560              
561 11         35 Getopt::Long::Configure(qw/pass_through no_auto_abbrev no_ignore_case/);
562              
563 11         557 my $reason;
564 11         19 my $optargs = {};
565 11         14 my @trigger;
566              
567 11         13 my $cmd = $self;
568              
569             # Start with the parents options
570 11         30 my @opts = map { @{ $_->opts } } $cmd->parents, $cmd;
  11         14  
  11         19  
571 11         20 my @args = @{ $cmd->args };
  11         19  
572              
573 11   66     73 OPTARGS: while ( @opts or @args ) {
574 11         26 while ( my $try = shift @opts ) {
575 13         53 my $result;
576 13         40 my $name = $try->name;
577              
578 13 50       54 if ( exists $source_hash->{$name} ) {
579 0         0 $result = delete $source_hash->{$name};
580             }
581             else {
582 13         50 GetOptionsFromArray( $source, $try->getopt => \$result );
583             }
584              
585 13 50 33     1725 if ( defined($result) and my $t = $try->trigger ) {
586 0         0 push @trigger, [ $t, $name ];
587             }
588              
589 13 100 66     53 if ( defined( $result //= $try->default ) ) {
    50          
590              
591 1 50       12 if ( 'CODE' eq ref $result ) {
592 0         0 tie $optargs->{$name}, 'OptArgs2::CODEREF', $optargs,
593             $name,
594             $result;
595             }
596             else {
597 1         12 $optargs->{$name} = $result;
598             }
599             }
600             elsif ( $try->required ) {
601 0         0 $name =~ s/_/-/g;
602 0   0     0 $reason //=
603             [ 'OptRequired', qq{missing required option "--$name"} ];
604             }
605             }
606              
607             # Sub command check
608 11 100 100     34 if ( @$source and my @subcmds = @{ $cmd->subcmds } ) {
  8         32  
609 7         12 my $result = $source->[0];
610 7 100       14 if ( $cmd->abbrev ) {
611 2         1166 require Text::Abbrev;
612             my %abbrev =
613 2         46 Text::Abbrev::abbrev( map { $_->name } @subcmds );
  2         6  
614 2   33     61 $result = $abbrev{$result} // $result;
615             }
616              
617 7         16 ( my $subcmd = $result ) =~ s/-/_/g;
618              
619 7 100       18 if ( exists $cmd->_subcmds->{$subcmd} ) {
620 4         6 shift @$source;
621 4         8 $cmd = $cmd->_subcmds->{$subcmd};
622 4         7 push( @opts, @{ $cmd->opts } );
  4         12  
623              
624             # Ignoring any remaining arguments
625 4         9 @args = @{ $cmd->args };
  4         8  
626              
627 4         16 next OPTARGS;
628             }
629             }
630              
631 7         31 while ( my $try = shift @args ) {
632 7         9 my $result;
633 7         24 my $name = $try->name;
634              
635 7 100       57 if (@$source) {
    50          
636 4 50 0     27 if (
      33        
      33        
637             # $try->isa ne 'SubCmd'
638             # and (
639             ( $source->[0] =~ m/^--\S/ )
640             or (
641             $source->[0] =~ m/^-\S/
642             and !(
643             $source->[0] =~ m/^-\d/
644             and ( $try->isa ne 'Num'
645             or $try->isa ne 'Int' )
646             )
647             )
648             )
649              
650             # )
651             {
652 0         0 my $o = shift @$source;
653 0   0     0 $reason //= [ 'OptUnknown', qq{unknown option "$o"} ];
654 0         0 last OPTARGS;
655             }
656              
657 4 50       14 if ( $try->greedy ) {
    50          
    50          
658              
659             # Interesting feature or not? "GREEDY... LATER"
660             # my @later;
661             # if ( @args and @$source > @args ) {
662             # push( @later, pop @$source ) for @args;
663             # }
664             # Should also possibly check early for post-greedy arg,
665             # except they might be wanted for display
666             # purposes
667              
668 0 0 0     0 if ( $try->isa eq 'ArrayRef' or $try->isa eq 'SubCmd' )
    0          
669             {
670 0         0 $result = [@$source];
671             }
672             elsif ( $try->isa eq 'HashRef' ) {
673             $result = {
674 0         0 map { split /=/, $_ }
  0         0  
675             split /,/, @$source
676             };
677             }
678             else {
679 0         0 $result = "@$source";
680             }
681              
682 0         0 $source = [];
683              
684             # $source = \@later;
685             }
686             elsif ( $try->isa eq 'ArrayRef' ) {
687 0         0 $result = [ shift @$source ];
688             }
689             elsif ( $try->isa eq 'HashRef' ) {
690             $result =
691 0         0 { map { split /=/, $_ } split /,/, shift @$source };
  0         0  
692             }
693             else {
694 4         13 $result = shift @$source;
695             }
696              
697             # TODO: type check using Param::Utils?
698             }
699             elsif ( exists $source_hash->{$name} ) {
700 0         0 $result = delete $source_hash->{$name};
701             }
702              
703 7 100 66     32 if ( defined( $result //= $try->default ) ) {
    100          
704             $reason //= [
705             'SubCmdUnknown',
706             "unknown $name: "
707             . (
708             ( 'ARRAY' eq ref $result ) ? $result->[0]
709             : ( 'HASH' eq ref $result ) ? (
710             join ',',
711 4 50 50     11 map { "$_=$result->{$_}" } keys %$result
  0 50 100     0  
    100          
712             )
713             : $result
714             )
715             ]
716             if $try->isa eq 'SubCmd' and not $try->fallthru;
717              
718 4 50       9 if ( 'CODE' eq ref $result ) {
719 0         0 tie $optargs->{$name}, 'OptArgs2::CODEREF', $optargs,
720             $name,
721             $result;
722             }
723             else {
724 4         33 $optargs->{$name} = $result;
725             }
726             }
727             elsif ( $try->required ) {
728 2   50     13 $reason //= ['ArgRequired'];
729             }
730             }
731             }
732              
733 11 50       43 if (@$source) {
    50          
734 0   0     0 $reason //= [
735             'UnexpectedOptArg',
736             "unexpected option(s) or argument(s): @$source"
737             ];
738             }
739             elsif ( my @unexpected = keys %$source_hash ) {
740 0   0     0 $reason //= [
741             'UnexpectedHashOptArg',
742             "unexpected HASH option(s) or argument(s): @unexpected"
743             ];
744             }
745              
746 11         35 $cmd->_values($optargs);
747              
748 11         19 map { $_->[0]->( $cmd, $optargs->{ $_->[1] } ) } @trigger;
  0         0  
749              
750 11 100       32 OptArgs2->throw_usage( $reason->[0],
751             $cmd->usage_string( OptArgs2::STYLE_USAGE, $reason->[1] ) )
752             if $reason;
753              
754 7         17 return ( $cmd->class, $optargs, ( $cmd->class . '.pm' ) =~ s!::!/!gr );
755             }
756              
757             sub _usage_tree {
758 0     0   0 my $self = shift;
759 0   0     0 my $depth = shift || 0;
760              
761             return [
762             $depth, $self->usage_string(OptArgs2::STYLE_HELPSUMMARY),
763             $self->comment
764             ],
765 0         0 map { $_->_usage_tree( $depth + 1 ) }
766 0         0 sort { $a->name cmp $b->name } @{ $self->subcmds };
  0         0  
  0         0  
767             }
768              
769             sub usage_string {
770 8     8 0 14 my $self = shift;
771 8   50     21 my $style = shift || OptArgs2::STYLE_USAGE;
772 8   100     26 my $error = shift // '';
773 8         10 my $usage = '';
774              
775 8 50       17 if ( $style eq OptArgs2::STYLE_HELPTREE ) {
776 0         0 my ( @w1, @w2 );
777             my @items = map {
778 0         0 $_->[0] = ' ' x ( $_->[0] * 3 );
  0         0  
779 0         0 push @w1, length( $_->[1] ) + length( $_->[0] );
780 0         0 push @w2, length $_->[2];
781 0         0 $_
782             } $self->_usage_tree;
783 0         0 my ( $w1, $w2 ) = ( max(@w1), max(@w2) );
784              
785 0         0 my $paged = OptArgs2::rows() < scalar @items;
786 0         0 my $cols = OptArgs2::cols();
787 0         0 my $usage = '';
788 0         0 my $spacew = 3;
789 0         0 my $space = ' ' x $spacew;
790              
791 0         0 foreach my $i ( 0 .. $#items ) {
792 0         0 my $overlap = $w1 + $spacew + $w2[$i] - $cols;
793 0 0 0     0 if ( $overlap > 0 and not $paged ) {
794 0         0 $items[$i]->[2] =
795             sprintf '%-.' . ( $w2[$i] - $overlap - 3 ) . 's%s',
796             $items[$i]->[2], '.' x 3;
797             }
798 0         0 $usage .= sprintf "%-${w1}s${space}%-s\n",
799             $items[$i]->[0] . $items[$i]->[1],
800             $items[$i]->[2];
801             }
802 0         0 return $usage;
803             }
804              
805 8         32 my @parents = $self->parents;
806 8         9 my @args = @{ $self->args };
  8         20  
807             my @opts =
808 8         15 sort { $a->name cmp $b->name } map { @{ $_->opts } } @parents,
  0         0  
  12         12  
  12         28  
809             $self;
810              
811 8         14 my $optargs = $self->_values;
812              
813             # Summary line
814 8 50 66     36 $usage .= join( ' ', map { $_->name } @parents ) . ' '
  0         0  
815             if @parents and $style ne OptArgs2::STYLE_HELPSUMMARY;
816 8         23 $usage .= $self->name;
817              
818 8         23 my ( $red, $grey, $reset ) = ( '', '', '' );
819 8 50       27 if ( $self->show_color ) {
820 0         0 $red = "\e[0;31m";
821 0         0 $grey = "\e[1;30m";
822 0         0 $reset = "\e[0m";
823              
824             # $red = "\e[0;31m";
825             # $yellow = "\e[0;33m";
826             }
827              
828 8 100       33 $error = $red . 'error:' . $reset . ' ' . $error . "\n\n"
829             if length $error;
830              
831 8         18 foreach my $arg (@args) {
832 4         5 $usage .= ' ';
833 4 100       11 $usage .= '[' unless $arg->required;
834 4         12 $usage .= uc $arg->name;
835 4 50       10 $usage .= '...' if $arg->greedy;
836 4 100       21 $usage .= ']' unless $arg->required;
837             }
838              
839 8 100       33 return $usage if $style eq OptArgs2::STYLE_HELPSUMMARY;
840              
841 4 50       10 $usage .= ' [OPTIONS...]' if @opts;
842 4         4 $usage .= "\n";
843              
844             # Synopsis
845 4 50 33     12 $usage .= "\n Synopsis:\n " . $self->comment . "\n"
846             if $style eq OptArgs2::STYLE_HELP and length $self->comment;
847              
848             # Build arguments
849 4         14 my @sargs;
850             my @uargs;
851 4         0 my $have_subcmd;
852              
853 4 50       14 if (@args) {
854 4         7 my $i = 0;
855 4         12 ARG: foreach my $arg (@args) {
856 4 50       11 if ( $arg->isa eq 'SubCmd' ) {
    0          
857             my ( $n, undef, undef, $c ) =
858             $arg->name_alias_type_comment(
859             $arg->show_default
860 4 50 0     16 ? eval { $optargs->{ $arg->name } // undef }
  0         0  
861             : ()
862             );
863 4         17 push( @sargs, [ ' ' . ucfirst($n) . ':', $c ] );
864             my @sorted_subs =
865 4         11 map { $_->[1] }
866 0         0 sort { $a->[0] cmp $b->[0] }
867 4         11 map { [ $_->name, $_ ] }
868 4 50       23 grep { $style eq OptArgs2::STYLE_HELP or !$_->hidden }
869 4         7 @{ $arg->cmd->subcmds };
  4         9  
870              
871 4         9 foreach my $subcmd (@sorted_subs) {
872 4         66 push(
873             @sargs,
874             [
875             ' '
876             . $subcmd->usage_string(
877             OptArgs2::STYLE_HELPSUMMARY),
878             $subcmd->comment
879             ]
880             );
881             }
882              
883 4         7 $have_subcmd++;
884 4         16 last ARG;
885             }
886             elsif ( 'OptArgRef' ne $arg->isa ) {
887 0 0       0 push( @uargs, [ ' Arguments:', '', '', '' ] ) if !$i;
888             my ( $n, $a, $t, $c ) = $arg->name_alias_type_comment(
889             $arg->show_default
890 0 0 0     0 ? eval { $optargs->{ $arg->name } // undef }
  0         0  
891             : ()
892             );
893 0         0 push( @uargs, [ ' ' . uc($n), $a, $t, $c ] );
894             }
895 0         0 $i++;
896             }
897             }
898              
899             # Build options
900 4         6 my @uopts;
901 4 50       10 if (@opts) {
902 4         16 push( @uopts, [ " Options:", '', '', '' ] );
903 4         17 foreach my $opt (@opts) {
904 4 50 33     19 next if $style ne OptArgs2::STYLE_HELP and $opt->hidden;
905             my ( $n, $a, $t, $c ) = $opt->name_alias_type_comment(
906             $opt->show_default
907 4 50 0     16 ? eval { $optargs->{ $opt->name } // undef }
  0         0  
908             : ()
909             );
910 4         17 push( @uopts, [ ' ' . $n, $a, uc($t), $c ] );
911             }
912             }
913              
914             # Width calculation for args and opts combined
915 4         9 my $w1 = max( 0, map { length $_->[0] } @uargs, @uopts );
  8         20  
916 4         8 my $w2 = max( 0, map { length $_->[1] } @uargs, @uopts );
  8         13  
917 4         7 my $w3 = max( 0, map { length $_->[2] } @uargs, @uopts );
  8         13  
918 4         6 my $w4 = max( 0, map { length $_->[0] } @sargs );
  8         20  
919 4         10 my $w5 = max( $w1 + $w2 + $w3, $w4 );
920              
921 4         10 my $format1 = "%-${w5}s %s\n";
922 4         15 my $format2 = "%-${w1}s %-${w2}s %-${w3}s";
923              
924             # Output Arguments
925 4 50       17 if (@sargs) {
926 4         6 $usage .= "\n";
927 4         7 foreach my $row (@sargs) {
928 8         79 $usage .= sprintf( $format1, @$row ) =~
929             s/^(\s+\w+\s)(.*?)(\s\s)/$1$grey$2$reset$3/r;
930             }
931             }
932              
933 4 50       12 if (@uargs) {
934 0         0 $usage .= "\n";
935 0         0 foreach my $row (@uargs) {
936 0         0 my $l = pop @$row;
937 0         0 $usage .= sprintf( $format1, sprintf( $format2, @$row ), $l );
938             }
939             }
940              
941             # Output Options
942 4 50       14 if (@uopts) {
943 4         8 $usage .= "\n";
944 4         6 foreach my $row (@uopts) {
945 8         13 my $l = pop @$row;
946 8         25 $usage .= sprintf( $format1, sprintf( $format2, @$row ), $l );
947             }
948             }
949              
950 4         32 return $error . 'usage: ' . $usage . "\n";
951             }
952              
953             }
954              
955             package OptArgs2::Cmd {
956             use OptArgs2::Cmd_CI
957             isa => 'OptArgs2::CmdBase',
958             has => {
959             class => { required => 1, },
960             name => {
961             default => sub {
962 4         10 my $x = $_[0]->class;
963 4 50       9 if ( $x eq 'main' ) {
964 0         0 require File::Basename;
965 0         0 File::Basename::basename($0),;
966             }
967             else {
968 4         16 $x =~ s/.*://;
969 4         9 $x =~ s/_/-/g;
970 4         12 $x;
971             }
972             },
973             },
974 3     3   1074 };
  3         6  
  3         27  
975              
976             our @CARP_NOT = @OptArgs2::CARP_NOT;
977              
978             sub BUILD {
979 6     6 0 9 my $self = shift;
980              
981 6 50       18 $self->add_opt(
982             isa => OptArgs2::STYLE_HELP,
983             show_default => 0,
984             ) unless $self->no_help;
985             }
986             }
987              
988             package OptArgs2::SubCmd {
989             use OptArgs2::SubCmd_CI
990 3         20 isa => 'OptArgs2::CmdBase',
991             has => {
992             name => { required => 1, },
993             parent => { required => 1, },
994 3     3   1018 };
  3         6  
995              
996             our @CARP_NOT = @OptArgs2::CARP_NOT;
997              
998             sub class {
999 8     8 0 10 my $self = shift;
1000 8         13 $self->parent->class . '::' . $self->name;
1001             }
1002             }
1003              
1004             1;
1005              
1006             __END__