File Coverage

blib/lib/Declare/CLI.pm
Criterion Covered Total %
statement 233 239 97.4
branch 96 120 80.0
condition 40 51 78.4
subroutine 34 36 94.4
pod 16 16 100.0
total 419 462 90.6


line stmt bran cond sub pod time code
1             package Declare::CLI;
2 10     10   2059830 use strict;
  10         40  
  10         510  
3 10     10   60 use warnings;
  10         20  
  10         440  
4              
5 10     10   90 use Carp qw/croak/;
  10         70  
  10         1050  
6 10     10   70 use Scalar::Util qw/blessed/;
  10         20  
  10         600  
7 10     10   60 use List::Util qw/max/;
  10         20  
  10         1850  
8              
9 10         100 use Exporter::Declare qw{
10             import
11             gen_default_export
12             default_export
13 10     10   70 };
  10         20  
14              
15             our $VERSION = 0.008;
16              
17             gen_default_export CLI_META => sub {
18             my ( $class, $caller ) = @_;
19             my $meta = $class->new();
20             $meta->{class} = $caller;
21 126     126   45918 return sub { $meta };
22             };
23              
24             default_export arg => sub {
25 13     13   2776821 my ( $meta, @params ) = _parse_params(@_);
26 13         114 $meta->add_arg(@params);
27             };
28              
29             default_export opt => sub {
30 41     41   6785391 my ( $meta, @params ) = _parse_params(@_);
31 41         235 $meta->add_opt(@params);
32             };
33              
34             default_export describe_opt => sub {
35 1     1   6 my ( $meta, @params ) = _parse_params(@_);
36 1         21 $meta->describe( 'opt' => @params );
37             };
38              
39             default_export describe_arg => sub {
40 1     1   4 my ( $meta, @params ) = _parse_params(@_);
41 1         4 $meta->describe( 'arg' => @params );
42             };
43              
44             default_export usage => sub {
45 2     2   22 my ( $meta, @params ) = _parse_params(@_);
46 2         26 $meta->usage(@params);
47             };
48              
49             for my $name (qw/ preparse parse process run handle /) {
50             default_export "${name}_cli" => sub {
51 28     28   28124 my $consumer = shift;
52 28         225 my $meta = $consumer->CLI_META;
53 28         653 return $meta->$name( $consumer, @_ );
54             };
55             }
56              
57             sub _parse_params {
58 58     58   623 my ( $first, @params ) = @_;
59              
60 58         188 my $ref = ref $first;
61 58         301 my $type = blessed $first;
62              
63             return ( $first->CLI_META, @params )
64 58 100 66     999 if ( $type || !$ref ) && eval { $first->can('CLI_META') };
  58   66     960  
65              
66 56         97 my $meta = eval { caller(2)->CLI_META };
  56         573  
67 56 50       375 croak "Could not find meta data object: $@"
68             unless $meta;
69              
70 56         264 return ( $meta, @_ );
71             }
72              
73 10     10 1 130 sub class { shift->{class} }
74 65     65 1 322 sub args { shift->{args} }
75 389     389 1 2392 sub opts { shift->{opts} }
76 30     30   180 sub _defaults { shift->{defaults} }
77              
78             sub new {
79 10     10 1 20 my $class = shift;
80 10         20 my %params = @_;
81 10         100 my $self = bless {args => {}, opts => {}, defaults => {}} => $class;
82              
83 10 50       30 $self->add_arg( $_ => $params{args}->{$_} ) for keys %{$params{args} || {}};
  10         130  
84              
85 10 50       20 $self->add_arg( $_ => $params{opts}->{$_} ) for keys %{$params{opts} || {}};
  10         80  
86              
87 10         40 return $self;
88             }
89              
90             sub describe {
91 2     2 1 4 my $self = shift;
92 2         9 my ( $type, $name, $desc ) = @_;
93              
94 2         5 my $meth = $type . 's';
95 2 50       7 croak "No such $type '$name'"
96             unless $self->$meth->{$name};
97              
98 2 50       8 $self->$meth->{$name}->{description} = $desc if $desc;
99              
100 2         5 return $self->$meth->{$name}->{description};
101             }
102              
103             sub valid_arg_params {
104 16     16 1 549 return qr/^(alias|description|handler)$/;
105             }
106              
107             sub add_arg {
108 13     13 1 32 my $self = shift;
109 13         45 my ( $name, @params ) = @_;
110 13 100       404 my %config = @params > 1 ? @params : ( handler => $params[0] );
111              
112 13 50       50 croak "arg '$name' already defined"
113             if $self->args->{$name};
114              
115 13         72 for my $prop ( keys %config ) {
116 16 50       56 next if $prop =~ $self->valid_arg_params;
117 0         0 croak "invalid arg property: '$prop'";
118             }
119              
120 13         70 $config{name} = $name;
121 13   100     571 $config{description} ||= "No Description.";
122              
123 13 50       60 croak "You must provide a handler"
124             unless $config{handler};
125              
126 13 100       67 if ( exists $config{alias} ) {
127 2 50       9 my $aliases =
128             ref $config{alias}
129             ? $config{alias}
130             : [$config{alias}];
131              
132 2         224 $config{_alias} = {map { $_ => 1 } @$aliases};
  2         20  
133              
134 2         5 for my $alias (@$aliases) {
135 2 50       6 croak "Cannot use alias '$alias', name is already taken by another arg."
136             if $self->args->{$alias};
137              
138 2         6 $self->args->{$alias} = \%config;
139             }
140             }
141              
142 13         88 $self->args->{$name} = \%config;
143             }
144              
145             sub valid_opt_params {
146 56     56 1 1163 return qr/^(alias|list|bool|default|check|transform|description|trigger)$/;
147             }
148              
149             sub add_opt {
150 41     41 1 99 my $self = shift;
151 41         201 my ( $name, %config ) = @_;
152              
153 41 50       107 croak "opt '$name' already defined"
154             if $self->opts->{$name};
155              
156 41         221 for my $prop ( keys %config ) {
157 56 50       180 next if $prop =~ $self->valid_opt_params;
158 0         0 croak "invalid opt property: '$prop'";
159             }
160              
161 41         158 $config{name} = $name;
162 41   100     374 $config{description} ||= "No Description.";
163              
164 41 50 66     256 croak "'check' cannot be used with 'bool'"
165             if $config{bool} && $config{check};
166              
167 41 50 66     195 croak "'transform' cannot be used with 'bool'"
168             if $config{bool} && $config{transform};
169              
170 41 100 100     501 croak "opt properties 'list' and 'bool' are mutually exclusive"
171             if $config{list} && $config{bool};
172              
173 39 100       167 if ( exists $config{default} ) {
174 4 50 33     15 croak "References cannot be used in default, wrap them in a sub."
175             if ref $config{default} && ref $config{default} ne 'CODE';
176 4         14 $self->_defaults->{$name} = $config{default};
177             }
178              
179 39 100       1323 if ( exists $config{check} ) {
180 18         53 my $ref = ref $config{check};
181 18 100 100     1235 croak "'$config{check}' is not a valid value for 'check'"
      100        
      100        
182             if ( $ref && $ref !~ m/^(CODE|Regexp)$/ )
183             || ( !$ref && $config{check} !~ m/^(file|dir|number)$/ );
184             }
185              
186 35 100       169 if ( exists $config{alias} ) {
187 4 100       17 my $aliases =
188             ref $config{alias}
189             ? $config{alias}
190             : [$config{alias}];
191              
192 4         9 $config{_alias} = {map { $_ => 1 } @$aliases};
  6         22  
193              
194 4         9 for my $alias (@$aliases) {
195 6 50       12 croak "Cannot use alias '$alias', name is already taken by another opt."
196             if $self->opts->{$alias};
197              
198 6         14 $self->opts->{$alias} = \%config;
199             }
200             }
201              
202 35         205 $self->opts->{$name} = \%config;
203             }
204              
205             sub _opt_value {
206 44     44   227 my $self = shift;
207 44         175 my ( $opt, $value, $cli ) = @_;
208              
209 44         94 my $spec = $self->opts->{$opt};
210              
211 44 100       144 if ( $spec->{bool} ) {
212 6 100       25 return $value if defined $value;
213 4 100       23 return $spec->{default} ? 0 : 1;
214             }
215              
216 38 100       149 my $val = defined $value ? $value : shift @$cli;
217              
218 38 100       241 return $spec->{list}
219             ? [split /\s*,\s*/, $val]
220             : $val;
221             }
222              
223             sub _validate {
224 46     46   133 my $self = shift;
225 46         71 my ( $opt, $value ) = @_;
226 46         103 my $spec = $self->opts->{$opt};
227              
228 46         81 my $check = $spec->{check};
229 46 100       482 return unless $check;
230 24   100     131 my $ref = ref $check || "";
231              
232 24         28 my @bad;
233              
234 24 100       119 if ( $ref eq 'Regexp' ) {
    100          
    100          
    100          
    50          
235 4         10 @bad = grep { $_ !~ $check } @$value;
  4         51  
236             }
237             elsif ( $ref eq 'CODE' ) {
238 4         9 @bad = grep { !$check->($_) } @$value;
  4         15  
239             }
240             elsif ( $check eq 'file' ) {
241 4         10 @bad = grep { !-f $_ } @$value;
  4         3579  
242             }
243             elsif ( $check eq 'dir' ) {
244 4         10 @bad = grep { !-d $_ } @$value;
  12         328  
245             }
246             elsif ( $check eq 'number' ) {
247 8         17 @bad = grep { m/\D/ } @$value;
  22         77  
248             }
249              
250 24 100       5966 return unless @bad;
251 10   66     58 my $type = $ref || $check;
252 10         173 die "Validation Failed for '$opt=$type': " . join( ", ", @bad ) . "\n";
253             }
254              
255             sub usage {
256 2     2 1 3 my $self = shift;
257              
258 2         3 my $arg_len = max map { length $_ } keys %{$self->args};
  6         28  
  2         6  
259 2         3 my $opt_len = max map { length $_ } keys %{$self->opts};
  10         14  
  2         6  
260              
261 2         5 my %seen;
262 10         19 my $opts = join "\n" => sort map {
263 2         4 my $spec = $self->opts->{$_};
264 10         16 my $name = $spec->{name};
265 10 100       38 my $value = $spec->{bool} ? "" : $spec->{list} ? "XXX,..." : "XXX";
    100          
266              
267 10 50       121 $seen{$name}++ ? () : sprintf(
268             " -%-${opt_len}s %-7s %s",
269             $name,
270             $value,
271             $spec->{description}
272             );
273 2         3 } keys %{$self->opts};
274              
275 2         7 %seen = ();
276 6         17 my $cmds = join "\n" => sort map {
277 2         4 my $spec = $self->args->{$_};
278 6         8 my $name = $spec->{name};
279              
280 6 50       33 $seen{$name}++ ? () : sprintf(
281             " %-${arg_len}s %s",
282             $name,
283             $spec->{description}
284             );
285 2         4 } keys %{$self->args};
286              
287 2         26 return <<" EOT";
288             Options:
289             $opts
290              
291             Arguments:
292             $cmds
293              
294             EOT
295             }
296              
297             sub preparse {
298 0     0 1 0 my $self = shift;
299 0         0 my (@cli) = @_;
300 0         0 return $self->_parse_cli( 'pre', @cli );
301             }
302              
303             sub parse {
304 26     26 1 49 my $self = shift;
305 26         105 my ( $consumer, @cli ) = @_;
306 26         403 my ( $opts, $args ) = $self->_parse_cli( 0, @cli );
307 22         161 $self->_process_opts( $consumer, $opts );
308 12         5695 return ( $opts, $args );
309             }
310              
311             sub run {
312 5     5 1 12 my $self = shift;
313 5         18 my ( $consumer, $opts, $args ) = @_;
314              
315 5 50 33     54 croak "No argument specified"
316             unless $args && @$args;
317              
318 5         12 my $arg = shift @$args;
319 5         17 my $name = $self->_item_name( 'argument', $self->args, $arg );
320              
321 4         13 my $handler = $self->args->{$name}->{handler};
322              
323 4 50       17 croak "Invalid argument '$arg'"
324             unless $handler;
325              
326 4         21 return $consumer->$handler( $name, $opts, @$args );
327             }
328              
329             sub handle {
330 8     8 1 10 my $self = shift;
331 8         53 my ( $consumer, @cli ) = @_;
332 8         25 my ( $opts, $args ) = $self->parse(@_);
333 1         4 return $self->run( $consumer, $opts, $args );
334             }
335              
336 0     0 1 0 sub process_cli { goto &process }
337              
338             sub process {
339 13     13 1 23 my $self = shift;
340 13         105 my ( $consumer, @cli ) = @_;
341              
342 13         1939 warn "process and process_cli are deprecated\n";
343              
344 13         107 my ( $opts, $args ) = $self->parse(@_);
345 6 50       305 $consumer->set_opts($opts) if $consumer->can('set_opts');
346 6 50       16790 $consumer->set_args($args) if $consumer->can('set_args');
347              
348 6 100 100     73 return $opts
349             unless @$args
350             && $self->_item_name( 'argument', $self->args, $args->[0] );
351              
352 2         10 return $self->run( $consumer, $opts, $args );
353             }
354              
355             sub _parse_cli {
356 26     26   48 my $self = shift;
357 26         73 my ( $pre, @cli ) = @_;
358              
359 26         328 my $args = [];
360 26         135 my $opts = {};
361 26         99 my $no_opts = 0;
362              
363 26         111 while ( my $item = shift @cli ) {
364 64         92 my ( $opt, $value );
365              
366 64 100       285 if ( $item eq '--' ) {
367 4         6 $no_opts++;
368 4         20 next;
369             }
370              
371 60 100 100     698 if ( $item =~ m/^-+([^-=]+)(?:=(.+))?$/ && !$no_opts ) {
372 48         492 my $key = $1;
373 48         175 $value = $2;
374 48         125 $opt = $self->_item_name( 'option', $self->opts, $key );
375 46 100 66     263 die "unknown option '$key'\n" unless $pre || $opt;
376             }
377              
378             # If we do not have an opt, push to args and go to next.
379 56 100       127 unless ($opt) {
380 12         35 push @$args => $item;
381 12         45 next;
382             }
383              
384 44         363 $value = $self->_opt_value(
385             $opt,
386             $value,
387             \@cli
388             );
389              
390 44 100       130 if ( $self->opts->{$opt}->{list} ) {
391 14         22 push @{$opts->{$opt}} => @$value;
  14         103  
392             }
393             else {
394 30         309 $opts->{$opt} = $value;
395             }
396             }
397              
398             # Add defaults for opts not provided
399 22         268 for my $opt ( keys %{$self->_defaults} ) {
  22         159  
400 8 100       28 next if exists $opts->{$opt};
401 4         8 my $val = $self->_defaults->{$opt};
402 4 50       19 $opts->{$opt} = ref $val ? $val->() : $val;
403             }
404              
405 22         73 return ( $opts, $args );
406             }
407              
408             sub _process_opts {
409 22     22   31 my $self = shift;
410 22         51 my ( $consumer, $opts ) = @_;
411              
412 22         139 for my $opt ( keys %$opts ) {
413 46         13814 my $values = $opts->{$opt};
414 46         60 my $list;
415              
416 46 100 66     217 if ( ref $values && ref $values eq 'ARRAY' ) {
417 12         34 $list = 1;
418             }
419             else {
420 34         43 $list = 0;
421 34         169 $values = [$values];
422             }
423              
424 46         109 my $transform = $self->opts->{$opt}->{transform};
425 46         308 my $trigger = $self->opts->{$opt}->{trigger};
426              
427 46 100       112 $values = [map { $consumer->$transform($_) } @$values]
  8         63  
428             if $transform;
429              
430 46         265 $self->_validate( $opt, $values );
431              
432 36 100       328 $opts->{$opt} = $list ? $values : $values->[0];
433              
434 36 100       198 $consumer->$trigger( $opt, $opts->{$opt}, $opts )
435             if $trigger;
436             }
437             }
438              
439             sub _item_name {
440 56     56   76 my $self = shift;
441 56         138 my ( $type, $hash, $key ) = @_;
442              
443             # Exact match
444 56 100       428 return $hash->{$key}->{name}
445             if $hash->{$key};
446              
447 15         92 my %matches = map { $hash->{$_}->{name} => 1 }
  59         806  
448 13         124 grep { m/^$key/ }
449 13         25 keys %{$hash};
450 13         43 my @matches = keys %matches;
451              
452 13 100       798 die "partial $type '$key' is ambiguous, could be: " . join( ", " => sort @matches ) . "\n"
453             if @matches > 1;
454              
455 9 100       52 return '' unless @matches;
456 7         27 return $matches[0];
457             }
458              
459             1;
460              
461             __END__