File Coverage

blib/lib/Smart/Options.pm
Criterion Covered Total %
statement 234 244 95.9
branch 112 124 90.3
condition 33 54 61.1
subroutine 31 31 100.0
pod 15 17 88.2
total 425 470 90.4


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