File Coverage

blib/lib/Smart/Options.pm
Criterion Covered Total %
statement 235 245 95.9
branch 112 124 90.3
condition 33 54 61.1
subroutine 31 31 100.0
pod 15 17 88.2
total 426 471 90.4


line stmt bran cond sub pod time code
1             package Smart::Options;
2 21     21   227333 use strict;
  21         51  
  21         571  
3 21     21   106 use warnings;
  21         48  
  21         525  
4 21     21   539 use 5.010001;
  21         83  
5             our $VERSION = '0.061';
6              
7             require Exporter;
8             our @ISA = qw(Exporter);
9             our @EXPORT = qw(argv);
10              
11 21     21   10732 use List::MoreUtils qw(uniq);
  21         178199  
  21         275  
12 21     21   30244 use Text::Table;
  21         388744  
  21         824  
13 21     21   12808 use File::Slurp;
  21         282273  
  21         52829  
14              
15             sub new {
16 62     62 1 48048     my $pkg = shift;
17 62         217     my %opt = @_;
18              
19 62         834     my $self = bless {
20                     alias => {},
21                     default => {},
22                     boolean => {},
23                     demand => {},
24                     usage => "Usage: $0",
25                     describe => {},
26                     type => {},
27                     subcmd => {},
28                     coerce => {},
29                     env => {},
30                     env_prefix => '',
31                 }, $pkg;
32              
33 62 100 100     524     if ($opt{add_help} // 1) {
34 59         327         $self->options(h => {
35                             alias => 'help',
36                             describe => 'show help',
37                         });
38 59         193         $self->{add_help} = 1;
39                 }
40              
41 62         271     $self;
42             }
43              
44             sub argv {
45 12     12 1 20517     Smart::Options->new->parse(@_);
46             }
47              
48             sub _set {
49 229     229   507     my $self = shift;
50 229         378     my $param = shift;
51              
52 229         585     my %args = @_;
53 229         544     for my $option (keys %args) {
54 230         673         $self->{$param}->{$option} = $args{$option};
55                 }
56              
57 229         742     $self;
58             }
59              
60 89     89 1 270 sub alias { shift->_set('alias', @_) }
61 7     7 1 40 sub default { shift->_set('default', @_) }
62 62     62 1 211 sub describe { shift->_set('describe', @_) }
63 67     67 1 193 sub type { shift->_set('type', @_) }
64 4     4 1 9 sub subcmd { shift->_set('subcmd', @_) }
65              
66             sub _set_flag {
67 14     14   30     my $self = shift;
68 14         25     my $param = shift;
69              
70 14         42     for my $option (@_) {
71 23         62         $self->{$param}->{$option} = 1;
72                 }
73              
74 14         47     $self;
75             }
76              
77 3     3 1 15 sub boolean { shift->_set_flag('boolean', @_) }
78 9     9 1 34 sub demand { shift->_set_flag('demand', @_) }
79 2     2 0 7 sub env { shift->_set_flag('env', @_) }
80              
81             sub options {
82 60     60 1 123     my $self = shift;
83              
84 60         205     my %args = @_;
85 60         313     while (my($opt, $setting) = each %args) {
86 60         218         for my $key (keys %$setting) {
87 121         490             $self->$key($opt, $setting->{$key});
88                     }
89                 }
90              
91 60         159     $self;
92             }
93              
94             sub coerce {
95 32     32 1 101     my ($self, $isa, $type, $generater) = @_;
96              
97 32         119     $self->{coerce}->{$isa} = {
98                     type => $type,
99                     generater => $generater,
100                 };
101              
102 32         187     $self;
103             }
104              
105 4     4 1 31 sub usage { $_[0]->{usage} = $_[1]; $_[0] }
  4         20  
106 2     2 0 7 sub env_prefix { $_[0]->{env_prefix} = $_[1]; $_[0] }
  2         6  
107              
108             sub _get_opt_desc {
109 13     13   36     my ($self, $option) = @_;
110              
111 13         39     my @opts = ($option);
112 13         60     while ( my($opt, $val) = each %{$self->{alias}} ) {
  30         102  
113 17 100       50         push @opts, $opt if $val eq $option;
114                 }
115              
116 13 100       47     return join(', ', map { (length($_) == 1 ? '-' : '--') . $_ } sort @opts);
  20         142  
117             }
118              
119             sub _get_describe {
120 13     13   41     my ($self, $option) = @_;
121              
122 13         34     my $desc = $self->{describe}->{$option};
123 13         29     while ( my($opt, $val) = each %{$self->{alias}} ) {
  30         117  
124 17 100 100     80         $desc ||= $self->{describe}->{$opt} if $val eq $option;
125                 }
126              
127 13 100       115     return $desc ? ucfirst($desc) : '';
128             }
129              
130             sub _get_default {
131 14     14   42     my ($self, $option) = @_;
132              
133 14         43     my $value = $self->{default}->{$option};
134 14         44     while ( my($opt, $val) = each %{$self->{alias}} ) {
  32         169  
135 18 100 66     83         $value ||= $self->{default}->{$opt} if $val eq $option;
136                 }
137              
138 14         81     $value;
139             }
140              
141             sub help {
142 6     6 1 25     my $self = shift;
143              
144 6         17     my $alias = $self->{alias};
145 6         18     my $demand = $self->{demand};
146 6         14     my $describe = $self->{describe};
147 6         17     my $default = $self->{default};
148 6         18     my $boolean = $self->{boolean};
149 6         27     my $help = $self->{usage} . "\n";
150              
151 6 50 100     53     if (scalar(keys %$demand) or scalar(keys %$describe)) {
152 6         16         my @opts;
153 6         91         for my $opt (uniq sort keys %$demand, keys %$describe, keys %$default, keys %$boolean, values %$alias) {
154 17 100       202             next if $alias->{$opt};
155                         push @opts, [
156                             $self->_get_opt_desc($opt),
157                             $self->_get_describe($opt),
158                             $boolean->{$opt} ? '[boolean]' : '',
159 13 100       56                 $demand->{$opt} ? '[required]' : '',
    100          
    100          
160 1         6                 $self->_get_default($opt) ? "[default: @{[$self->_get_default($opt)]}]" : '',
161                         ];
162                     }
163              
164 6         22         my $sep = \' ';
165 6         20         $help .= "\nOptions:\n";
166 6         75         $help .= Text::Table->new( $sep, '', $sep, '', $sep, '', $sep, '', $sep, '' )
167                                         ->load(@opts)->stringify . "\n";
168 6 100       89857         if (keys %{$self->{subcmd}}) {
  6         100  
169 1         3             $help .= "Implemented commands are:\n";
170 1         2             $help .= " " . join(', ', sort keys %{$self->{subcmd}}) . "\n\n";
  1         9  
171                     }
172                 }
173              
174 6         409     $help;
175             }
176              
177             sub showHelp {
178 4     4 1 18     my ($self, $fh) = @_;
179 4   33     38     $fh //= *STDERR;
180              
181 4         26     print $fh $self->help;
182              
183             }
184              
185             sub _set_v2a {
186 81     81   308     my ($argv, $key, $value, $k) = @_;
187              
188 81 100       308     if ($k) {
    100          
189 5   100     47         $argv->{$key} //= {};
190 5         19         _set_v2a($argv->{$key}, $k, $value);
191                 }
192                 elsif (exists $argv->{$key}) {
193 8 100       36         if (ref($argv->{$key})) {
194 2         5             push @{$argv->{$key}}, $value;
  2         36  
195                     }
196                     else {
197 6         31             $argv->{$key} = [ $argv->{$key}, $value ];
198                     }
199                 }
200                 else {
201 68         265         $argv->{$key} = $value;
202                 }
203             }
204              
205             sub _get_real_name {
206 178     178   432     my ($self, $opt) = @_;
207              
208 178         619     while (my $name = $self->{alias}->{$opt}) {
209 12         57         $opt = $name;
210                 }
211 178         437     return $opt;
212             }
213              
214             sub _load_config {
215 2     2   7     my ($self, $argv, $file) = @_;
216              
217 2         12     for my $line (read_file($file)) {
218 12 100       334         next if $line =~ /^\[/; # section
219 8 100       18         next if $line =~ /^;/; # comment
220 6 50       19         next if $line !~ /=/; # bad format;
221              
222 6         14         chomp($line);
223 6 50       20         if ($line =~ /^(.+?[^\\])=(.*)$/) {
224 6         20             $argv->{$1} = $2;
225                     }
226                 }
227             }
228              
229             sub parse {
230 64     64 1 3406     my $self = shift;
231 64 100       286     push @_, @ARGV unless @_;
232              
233 64         145     my $argv = {};
234 64         144     my @args;
235 64         152     my $boolean = $self->{boolean};
236              
237 64         139     my $key;
238                 my $nest_key;
239 64         139     my $stop = 0;
240 64         163     for my $arg (@_) {
241 147 100       414         if ($stop) {
242 20         43             push @args, $arg;
243 20         38             next;
244                     }
245 127 100       915         if ($arg =~ /^--((?:\w|-|\.)+)=(.+)$/) {
    100          
    100          
    100          
246 50         261             my ($opt, $k) = split(/\./, $1);
247 50         176             my $option = $self->_get_real_name($opt);
248 50 100       141             if ($k) {
249 4         15                 _set_v2a($argv, $option, $2, $k);
250                         } else {
251 46         149                 _set_v2a($argv, $option, $2);
252                         }
253                     }
254                     elsif ($arg =~ /^(-(\w)|--((?:\w|-|\.)+))$/) {
255 37 100       123             if ($key) {
256 2         6                 $argv->{$key} = 1;
257                         }
258 37   66     183             my $opt = $2 // $3;
259 37 100       123             if ($opt =~ /^no\-(.+)$/) {
260 1         5                 my $option = $self->_get_real_name($1);
261 1         3                 $argv->{$option} = 0;
262 1         4                 next;
263                         }
264 36         167             ($opt, my $k) = split(/\./, $opt);
265 36         149             my $option = $self->_get_real_name($opt);
266 36 100       116             if ($boolean->{$option}) {
267 3 50       11                 if ($k) {
268 0   0     0                     $argv->{$option} //= {};
269 0         0                     $argv->{$option}->{$k} = 1;
270                             } else {
271 3         12                     $argv->{$option} = 1;
272                             }
273                         }
274                         else {
275 33         64                 $key = $option;
276 33         79                 $nest_key = $k;
277                         }
278                     }
279                     elsif ($arg =~ /^-(\w(?:\w|-|\.)+)$/) {
280 2 50       10             if ($key) {
281 0         0                 $argv->{$key} = 1;
282                         }
283 2         6             my $opt_str = $1;
284 2 100       8             if ($opt_str =~ /^(.)([0-9])+$/) {
285 1         5                 my $option = $self->_get_real_name($1);
286 1         3                 $argv->{$option} = $2;
287                         } else {
288 1         4                 for (split //, $opt_str) {
289 3         8                     my $option = $self->_get_real_name($_);
290 3         8                     $argv->{$option} = 1;
291                             }
292                         }
293                     }
294                     elsif ($arg =~ /^--$/) {
295             # stop parsing
296 4         20             $stop = 1;
297 4         14             next;
298                     }
299                     else {
300 34 100       89             if ($key) {
301 26 100       77                 if ($nest_key) {
302 1         5                     _set_v2a($argv, $key, $arg, $nest_key);
303                             } else {
304 25         82                     _set_v2a($argv, $key, $arg);
305                             }
306             # reset
307 26         71                 $key = $nest_key = undef;
308                         }
309                         else {
310 8 100 100     33                 if (!scalar(@args) && keys %{$self->{subcmd}}) {
  4         21  
311 1 50       3                     if ( $self->{subcmd}->{$arg} ) {
312 1         3                         $argv->{command} = $arg;
313 1         2                         $stop = 1;
314 1         2                         next;
315                                 }
316                                 else {
317 0         0                         die "sub command '$arg' not defined.";
318                                 }
319                             }
320              
321 7         22                 push @args, $arg;
322                         }
323                     }
324                 }
325 64 100       190     if ($key) {
326 6 50       20         if ($nest_key) {
327 0   0     0             $argv->{$key} //= {};
328 0         0             $argv->{$key}->{$nest_key} = 1;
329                     } else {
330 6         19             $argv->{$key} = 1;
331                     }
332                 }
333              
334 64 100 100     456     if (my $parser = $self->{subcmd}->{$argv->{command}||''}) {
335 1         9         $argv->{cmd_option} = $parser->parse(@args);
336                 } else {
337 63         185         $argv->{_} = \@args;
338                 }
339              
340 64         130     for my $env (keys %{$self->{env}}) {
  64         242  
341 3 100       14         if (defined($ENV{uc($self->{env_prefix}."_$env")})) {
342 2         5             my $option = $self->_get_real_name($env);
343 2   66     10             $argv->{$option} //= $ENV{uc($self->{env_prefix}."_$env")};
344                     }
345                 }
346              
347 64         149     while (my ($key, $val) = each %{$self->{default}}) {
  71         395  
348 7         32         my $opt = $self->_get_real_name($key);
349 7 100 66     42         if (ref($val) && ref($val) eq 'CODE') {
350 1   33     7             $argv->{$opt} //= $val->();
351                     }
352                     else {
353 6   66     50             $argv->{$opt} //= $val;
354                     }
355                 }
356              
357 64         143     while (my ($key, $val) = each %{$self->{type}}) {
  137         504  
358 73 100       245         next if $val ne 'Config';
359 30 100 66     174         next if !($argv->{$key}) || !(-f $argv->{$key});
360 2         11         $self->_load_config($argv, delete $argv->{$key});
361                 }
362              
363 64         147     for my $key (keys %{$self->{demand}}) {
  64         189  
364 9         31         my $opt = $self->_get_real_name($key);
365 9 100       41         if (!$argv->{$opt}) {
366 3         16             $self->showHelp;
367 3         52             print STDERR "\nMissing required arguments: $opt\n";
368 3         89             die;
369                     }
370                 }
371              
372 61         151     for my $key (keys %{$self->{type}}) {
  61         175  
373 69         169         my $opt = $self->_get_real_name($key);
374 69         144         my $type = $self->{type}->{$key};
375 69 100       210         if (my $c = $self->{coerce}->{$type}) {
376 5         9             $type = $c->{type};
377 5         17             $argv->{$opt} = $c->{generater}->($argv->{$opt});
378                     }
379 69         155         my $check = 0;
380 69 50       418         if ($type eq 'Bool') {
    100          
    100          
    100          
    100          
    100          
381 0   0     0             $argv->{$opt} //= 0;
382 0 0       0             $check = ($argv->{$opt} =~ /^(0|1)$/) ? 1 : 0;
383                     } elsif ($type eq 'Str') {
384 3         7             $check = 1;
385                     } elsif ($type eq 'Int') {
386 19 100       45             if ($argv->{$opt}) {
387 18 100       91                 $check = ($argv->{$opt} =~ /^\-?\d+$/) ? 1 : 0;
388                         } else {
389 1         2                 $check = 1;
390                         }
391                     } elsif ($type eq 'Num') {
392 7 100       24             if ($argv->{$opt}) {
393 5 100       41                 $check = ($argv->{$opt} =~ /^\-?\d+(\.\d+)$/) ? 1 : 0;
394                         } else {
395 2         3                 $check = 1;
396                         }
397                     } elsif ($type eq 'ArrayRef') {
398 10   50     40             $argv->{$opt} //= [];
399 10 100       33             unless (ref($argv->{$opt})) {
400 2         11                 $argv->{$opt} = [$argv->{$opt}];
401                         }
402 10 100       35             $check = (ref($argv->{$opt}) eq 'ARRAY') ? 1 : 0;
403                     } elsif ($type eq 'HashRef') {
404 2   50     8             $argv->{$opt} //= {};
405 2 100       10             $check = (ref($argv->{$opt}) eq 'HASH') ? 1 : 0;
406                     } elsif ('Config') {
407 28 50 33     117             if ($argv->{$opt} && !(-f $argv->{$opt})) {
408 0         0                 die "cannot load config file '@{[$argv->{$opt}]}\n";
  0         0  
409                         }
410 28         58             $check = 1;
411                     } else {
412                         die "cannot find type constraint '$type'\n";
413                     }
414 69 100       215         unless ($check) {
415 6         11             die "Value '@{[$argv->{$opt}]}' invalid for option $opt($type)\n";
  6         84  
416                     }
417                 }
418              
419 55 50 66     228     if ($argv->{help} && $self->{add_help}) {
420 1         4         $self->showHelp;
421 1         48         die;
422                 }
423              
424 54         257     $argv;
425             }
426              
427              
428             1;
429             __END__
430            
431             =encoding utf8
432            
433             =head1 NAME
434            
435             Smart::Options - smart command line options processor
436            
437             =head1 SYNOPSIS
438            
439             use Smart::Options;
440            
441             my $argv = Smart::Options->new->argv;
442            
443             if ($argv->{rif} - 5 * $argv->{xup} > 7.138) {
444             say 'Buy more fiffiwobbles';
445             }
446             else {
447             say 'Sell the xupptumblers';
448             }
449            
450             # $ ./example.pl --rif=55 --xup=9.52
451             # Buy more fiffiwobbles
452             #
453             # $ ./example.pl --rif 12 --xup 8.1
454             # Sell the xupptumblers
455            
456             =head1 DESCRIPTION
457            
458             Smart::Options is a library for option parsing for people tired option parsing.
459             This module is analyzed as people interpret an option intuitively.
460            
461             =head1 METHOD
462            
463             =head2 new()
464            
465             Create a parser object.
466            
467             use Smart::Options;
468            
469             my $argv = Smart::Options->new->parse(qw(-x 10 -y 2));
470            
471             =head2 parse(@args)
472            
473             parse @args. return hashref of option values.
474             if @args is empty Smart::Options use @ARGV
475            
476             =head2 argv(@args)
477            
478             shortcut method. this method auto export.
479            
480             use Smart::Options;
481             say argv(qw(-x 10))->{x};
482            
483             is the same as
484            
485             use Smart::Options ();
486             Smart::Options->new->parse(qw(-x 10))->{x};
487            
488             =head2 alias($alias, $option)
489            
490             set alias for option. you can use "$option" field of argv.
491            
492             use Smart::Options;
493            
494             my $argv = Smart::Options->new->alias(f => 'file')->parse(qw(-f /etc/hosts));
495             $argv->{file} # => '/etc/hosts'
496            
497             =head2 default($option, $default_value)
498            
499             set default value for option.
500            
501             use Smart::Options;
502            
503             my $argv = Smart::Options->new->default(y => 5)->parse(qw(-x 10));
504             $argv->{x} + $argv->{y} # => 15
505            
506             =head2 describe($option, $msg)
507            
508             set option help message.
509            
510             use Smart::Options;
511             my $opt = Smart::Options->new()->alias(f => 'file')->describe('Load a file');
512             say $opt->help;
513            
514             # Usage: ./example.pl
515             #
516             # Options:
517             # -f, --file Load a file
518             #
519            
520             =head2 boolean($option, $option2, ...)
521            
522             interpret 'option' as a boolean.
523            
524             use Smart::Options;
525            
526             my $argv = Smart::Options->new->parse(qw(-x 11 -y 10));
527             $argv->{x} # => 11
528            
529             my $argv2 = Smart::Options->new->boolean('x')->parse(qw(-x 11 -y 10));
530             $argv2->{x} # => true (1)
531            
532             =head2 demand($option, $option2, ...)
533            
534             show usage (showHelp()) and exit if $option wasn't specified in args.
535            
536             use Smart::Options;
537             my $opt = Smart::Options->new()->alias(f => 'file')
538             ->demand('file')
539             ->describe('Load a file');
540             $opt->argv(); # => exit
541            
542             # Usage: ./example.pl
543             #
544             # Options:
545             # -f, --file Load a file [required]
546             #
547            
548             =head2 options($key => $settings, ...)
549            
550             use Smart::Options;
551             my $opt = Smart::Options->new()
552             ->options( f => { alias => 'file', default => '/etc/passwd' } );
553            
554             is the same as
555            
556             use Smart::Options;
557             my $opt = Smart::Options->new()
558             ->alias(f => 'file')
559             ->default(f => '/etc/passwd');
560            
561             =head2 type($option => $type)
562            
563             set type check for option value
564            
565             use Smart::Options;
566             my $opt = Smart::Options->new()->type(foo => 'Int');
567            
568             $opt->parse('--foo=bar') # => fail
569             $opt->parse('--foo=3.14') # => fail
570             $opt->parse('--foo=1') # => ok
571            
572             support type is here.
573            
574             Bool
575             Str
576             Int
577             Num
578             ArrayRef
579             HashRef
580             Config
581            
582             =head3 Config
583            
584             'Config' is special type.
585             The contents will be read into each option if a file name is specified as a Config type option.
586            
587             use Smart::Options;
588             my $opt = Smart::Options->new()->type(conf => 'Config');
589             $opt->parse(qw(--conf=.optrc));
590            
591             config file format is simple. see http://en.wikipedia.org/wiki/INI_file
592            
593             ; this is comment
594             [section]
595             key=value
596             key2=value2
597            
598             =head2 coerce( $newtype => $sourcetype, $generator )
599            
600             define new type and convert logic.
601            
602             use Smart::Options;
603             use Path::Class; # export 'file'
604             my $opt = Smart::Options->new()->coerce(File => 'Str', sub { file($_[0]) })
605             ->type(file => 'File');
606            
607             $opt->parse('--foo=/etc/passwd');
608             $argv->{file} # => Path::Class::File instance
609            
610             =head2 usage
611            
612             set a usage message to show which command to use. default is "Usage: $0".
613            
614             =head2 help
615            
616             return help message string
617            
618             =head2 showHelp($fh)
619            
620             print usage message. default output STDERR.
621            
622             =head2 subcmd($cmd => $parser)
623            
624             set a sub command. $parser is another Smart::Option object.
625            
626             use Smart::Options;
627             my $opt = Smart::Options->new()
628             ->subcmd(add => Smart::Options->new())
629             ->subcmd(minus => Smart::Options->new());
630            
631             =head1 DSL
632            
633             see also L<Smart::Options::Declare>
634            
635             =head1 PARSING TRICKS
636            
637             =head2 stop parsing
638            
639             use '--' to stop parsing.
640            
641             use Smart::Options;
642             use Data::Dumper;
643            
644             my $argv = argv(qw(-a 1 -b 2 -- -c 3 -d 4));
645             warn Dumper($argv);
646            
647             # $VAR1 = {
648             # 'a' => '1',
649             # 'b' => '2',
650             # '_' => [
651             # '-c',
652             # '3',
653             # '-d',
654             # '4'
655             # ]
656             # };
657            
658             =head2 negate fields
659            
660             '--no-key' set false to $key.
661            
662             use Smart::Options;
663             argv(qw(-a --no-b))->{b}; # => 0
664            
665             =head2 duplicates
666            
667             If set flag multiple times it will get arrayref.
668            
669             use Smart::Options;
670             argv(qw(-x 1 -x 2 -x 3))->{x}; # => [1, 2, 3]
671            
672             =head2 dot notation
673            
674             use Smart::Optuions;
675             argv(qw(--foo.x 1 --foo.y 2)); # => { foo => { x => 1, y => 2 } }
676            
677             =head1 AUTHOR
678            
679             Kan Fushihara E<lt>kan.fushihara@gmail.comE<gt>
680            
681             =head1 SEE ALSO
682            
683             https://www.npmjs.com/package/minimist
684            
685             L<GetOpt::Casual>, L<opts>, L<GetOpt::Compat::WithCmd>
686            
687             =head1 LICENSE
688            
689             Copyright (C) Kan Fushihara
690            
691             This library is free software; you can redistribute it and/or modify
692             it under the same terms as Perl itself.
693            
694             =cut
695