File Coverage

blib/lib/Declare/CLI.pm
Criterion Covered Total %
statement 233 239 97.4
branch 96 120 80.0
condition 39 51 76.4
subroutine 34 36 94.4
pod 16 16 100.0
total 418 462 90.4


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