File Coverage

blib/lib/Getopt/Panjang.pm
Criterion Covered Total %
statement 110 117 94.0
branch 63 78 80.7
condition 6 6 100.0
subroutine 6 6 100.0
pod 1 1 100.0
total 186 208 89.4


line stmt bran cond sub pod time code
1             package Getopt::Panjang;
2              
3             our $DATE = '2015-09-15'; # DATE
4             our $VERSION = '0.02'; # VERSION
5              
6 1     1   1024 use 5.010001;
  1         4  
7 1     1   6 use strict 'subs', 'vars';
  1         3  
  1         1705  
8             # IFUNBUILT
9             # use warnings;
10             # END IFUNBUILT
11              
12             our %SPEC;
13             our @EXPORT = qw();
14             our @EXPORT_OK = qw(get_options);
15              
16             sub import {
17 1     1   9 my $pkg = shift;
18 1         4 my $caller = caller;
19 1 50       4 my @imp = @_ ? @_ : @EXPORT;
20 1         2 for my $imp (@imp) {
21 1 50       3 if (grep {$_ eq $imp} (@EXPORT, @EXPORT_OK)) {
  1         4  
22 1         2 *{"$caller\::$imp"} = \&{$imp};
  1         33  
  1         3  
23             } else {
24 0         0 die "$imp is not exported by ".__PACKAGE__;
25             }
26             }
27             }
28              
29             $SPEC{get_options} = {
30             v => 1.1,
31             summary => 'Parse command-line options',
32             args => {
33             argv => {
34             summary => 'Command-line arguments, which will be parsed',
35             description => <<'_',
36              
37             If unspecified, will default to `@ARGV`.
38              
39             _
40             schema => ['array*', of=>'str*'],
41             pos => 0,
42             greedy => 1,
43             },
44             spec => {
45             summary => 'Options specification',
46             description => <<'_',
47              
48             Similar like `Getopt::Long` and `Getopt::Long::Evenless`, this argument should
49             be a hash. The keys should be option name specifications, while the values
50             should be option handlers.
51              
52             Option name specification is like in `Getopt::Long::EvenLess`, e.g. `name`,
53             `name=s`, `name|alias=s`.
54              
55             Option handler will be passed `%args` with the possible keys as follow: `name`
56             (str, option name), `value` (any, option value). A handler can die with an error
57             message to signify failed validation for the option value.
58              
59             _
60             schema => ['hash*', values=>'code*'],
61             req => 1,
62             },
63             },
64             result => {
65             description => <<'_',
66              
67             Will return 200 on parse success. If there is an error, like missing option
68             value or unknown option, will return 500. The result metadata will contain more
69             information about the error.
70              
71             _
72             },
73             };
74             sub get_options {
75 18     18 1 52717 my %args = @_;
76              
77             # XXX schema
78 18         20 my $argv;
79 18 50       51 if ($args{argv}) {
80 18 50       57 ref($args{argv}) eq 'ARRAY' or return [400, "argv is not an array"];
81 18         25 $argv = $args{argv};
82             } else {
83 0         0 $argv = \@ARGV;
84             }
85 18         26 my $spec = $args{spec};
86 18 50       45 ref($args{spec}) eq 'HASH' or return [400, "spec is not a hash"];
87 18         49 for (keys %$spec) {
88             return [400, "spec->{$_} is not a coderef"]
89 27 50       77 unless ref($spec->{$_}) eq 'CODE';
90             }
91              
92 18         21 my %spec_by_opt_name;
93 18         36 for (keys %$spec) {
94 27         32 my $orig = $_;
95 27         92 s/=[fios]\@?\z//;
96 27         50 s/\|.+//;
97 27         58 $spec_by_opt_name{$_} = $orig;
98             }
99              
100             my $code_find_opt = sub {
101 25     25   39 my ($wanted, $short_mode) = @_;
102 25         26 my @candidates;
103             OPT_SPEC:
104 25         49 for my $speckey (keys %$spec) {
105 36         93 $speckey =~ s/=[fios]\@?\z//;
106 36         79 my @opts = split /\|/, $speckey;
107 36         53 for my $o (@opts) {
108 46 100 100     810 next if $short_mode && length($o) > 1;
109 36 100       98 if ($o eq $wanted) {
    100          
110             # perfect match, we immediately go with this one
111 19         31 @candidates = ($opts[0]);
112 19         41 last OPT_SPEC;
113             } elsif (index($o, $wanted) == 0) {
114             # prefix match, collect candidates first
115 3         6 push @candidates, $opts[0];
116 3         8 next OPT_SPEC;
117             }
118             }
119             }
120 25 100       519 if (!@candidates) {
    100          
121 4         19 return [404, "Unknown option '$wanted'", undef,
122             {'func.unknown_opt' => $wanted}];
123             } elsif (@candidates > 1) {
124 1         11 return [300, "Option '$wanted' is ambiguous", undef, {
125             'func.ambiguous_opt' => $wanted,
126             'func.ambiguous_candidates' => [sort @candidates],
127             }];
128             }
129 20         53 return [200, "OK", $candidates[0]];
130 18         77 };
131              
132             my $code_set_val = sub {
133 17     17   26 my $name = shift;
134              
135 17         23 my $speckey = $spec_by_opt_name{$name};
136 17         21 my $handler = $spec->{$speckey};
137              
138 17         22 eval {
139 17 100       54 $handler->(
140             name => $name,
141             value => (@_ ? $_[0] : 1),
142             );
143             };
144 17 100       123 if ($@) {
145 1         5 return [400, "Invalid value for option '$name': $@", undef,
146             {'func.val_invalid_opt' => $name}];
147             } else {
148 16         37 return [200];
149             }
150 18         59 };
151              
152 18         25 my %unknown_opts;
153             my %ambiguous_opts;
154 0         0 my %val_missing_opts;
155 0         0 my %val_invalid_opts;
156              
157 18         23 my $i = -1;
158 18         19 my @remaining;
159             ELEM:
160 18         44 while (++$i < @$argv) {
161 26 100       172 if ($argv->[$i] eq '--') {
    100          
    100          
162              
163 2         7 push @remaining, @{$argv}[$i+1 .. @$argv-1];
  2         4  
164 2         5 last ELEM;
165              
166             } elsif ($argv->[$i] =~ /\A--(.+?)(?:=(.*))?\z/) {
167              
168 18         48 my ($used_name, $val_in_opt) = ($1, $2);
169 18         35 my $findres = $code_find_opt->($used_name);
170 18 100       63 if ($findres->[0] == 404) { # unknown opt
    100          
    50          
171 4         9 push @remaining, $argv->[$i];
172 4         26 $unknown_opts{ $findres->[3]{'func.unknown_opt'} }++;
173 4         15 next ELEM;
174             } elsif ($findres->[0] == 300) { # ambiguous
175             $ambiguous_opts{ $findres->[3]{'func.ambiguous_opt'} } =
176 1         4 $findres->[3]{'func.ambiguous_candidates'};
177 1         4 next ELEM;
178             } elsif ($findres->[0] != 200) {
179 0         0 return [500, "An unexpected error occurs", undef, {
180             'func._find_opt_res' => $findres,
181             }];
182             }
183 13         16 my $opt = $findres->[2];
184              
185 13         20 my $speckey = $spec_by_opt_name{$opt};
186             # check whether option requires an argument
187 13 50       37 if ($speckey =~ /=[fios]\@?\z/) {
188 13 100       25 if (defined $val_in_opt) {
189             # argument is taken after =
190 2 100       5 if (length $val_in_opt) {
191 1         3 my $setres = $code_set_val->($opt, $val_in_opt);
192 1 50       7 $val_invalid_opts{$opt} = $setres->[1]
193             unless $setres->[0] == 200;
194             } else {
195 1         3 $val_missing_opts{$used_name}++;
196 1         4 next ELEM;
197             }
198             } else {
199 11 100       26 if ($i+1 >= @$argv) {
200             # we are the last element
201 1         3 $val_missing_opts{$used_name}++;
202 1         3 last ELEM;
203             }
204 10         9 $i++;
205 10         21 my $setres = $code_set_val->($opt, $argv->[$i]);
206 10 100       52 $val_invalid_opts{$opt} = $setres->[1]
207             unless $setres->[0] == 200;
208             }
209             } else {
210 0         0 my $setres = $code_set_val->($opt);
211 0 0       0 $val_invalid_opts{$opt} = $setres->[1]
212             unless $setres->[0] == 200;
213             }
214              
215             } elsif ($argv->[$i] =~ /\A-(.*)/) {
216              
217 4         11 my $str = $1;
218             SHORT_OPT:
219 4         14 while ($str =~ s/(.)//) {
220 7         13 my $used_name = $1;
221 7         14 my $findres = $code_find_opt->($1, 'short');
222 7 50       18 next SHORT_OPT unless $findres->[0] == 200;
223 7         8 my $opt = $findres->[2];
224              
225 7         12 my $speckey = $spec_by_opt_name{$opt};
226             # check whether option requires an argument
227 7 100       20 if ($speckey =~ /=[fios]\@?\z/) {
228 4 100       9 if (length $str) {
229             # argument is taken from $str
230 2         5 my $setres = $code_set_val->($opt, $str);
231 2 50       5 $val_invalid_opts{$opt} = $setres->[1]
232             unless $setres->[0] == 200;
233 2         9 next ELEM;
234             } else {
235 2 100       8 if ($i+1 >= @$argv) {
236             # we are the last element
237 1         3 $val_missing_opts{$used_name}++;
238 1         3 last ELEM;
239             }
240             # take the next element as argument
241 1         2 $i++;
242 1         3 my $setres = $code_set_val->($opt, $argv->[$i]);
243 1 50       8 $val_invalid_opts{$opt} = $setres->[1]
244             unless $setres->[0] == 200;
245             }
246             } else {
247 3         6 my $setres = $code_set_val->($opt);
248 3 50       3958 $val_invalid_opts{$opt} = $setres->[1]
249             unless $setres->[0] == 200;
250             }
251             }
252              
253             } else { # argument
254              
255 2         5 push @remaining, $argv->[$i];
256 2         5 next;
257              
258             }
259             }
260              
261             RETURN:
262 18   100     121 my $success =
263             !keys(%unknown_opts) &&
264             !keys(%ambiguous_opts) &&
265             !keys(%val_missing_opts) &&
266             !keys(%val_invalid_opts);
267 18 100       552 [$success ? 200 : 500,
    100          
    100          
    100          
    100          
    100          
268             $success ? "OK" : "Error in parsing",
269             undef, {
270             'func.remaining_argv' => \@remaining,
271             ('func.unknown_opts' => \%unknown_opts )
272             x (keys(%unknown_opts) ? 1:0),
273             ('func.ambiguous_opts' => \%ambiguous_opts )
274             x (keys(%ambiguous_opts) ? 1:0),
275             ('func.val_missing_opts' => \%val_missing_opts)
276             x (keys(%val_missing_opts) ? 1:0),
277             ('func.val_invalid_opts' => \%val_invalid_opts)
278             x (keys(%val_invalid_opts) ? 1:0),
279             }];
280             }
281              
282             1;
283             # ABSTRACT: Parse command-line options
284              
285             __END__