File Coverage

blib/lib/Getopt/Panjang.pm
Criterion Covered Total %
statement 127 134 94.7
branch 70 88 79.5
condition 6 6 100.0
subroutine 6 6 100.0
pod 1 1 100.0
total 210 235 89.3


line stmt bran cond sub pod time code
1             package Getopt::Panjang;
2              
3             our $DATE = '2015-09-15'; # DATE
4             our $VERSION = '0.04'; # VERSION
5              
6 1     1   849 use 5.010001;
  1         3  
7 1     1   4 use strict 'subs', 'vars';
  1         2  
  1         1870  
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   7 my $pkg = shift;
18 1         3 my $caller = caller;
19 1 50       5 my @imp = @_ ? @_ : @EXPORT;
20 1         2 for my $imp (@imp) {
21 1 50       2 if (grep {$_ eq $imp} (@EXPORT, @EXPORT_OK)) {
  1         5  
22 1         1 *{"$caller\::$imp"} = \&{$imp};
  1         30  
  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 41095 my %args = @_;
76              
77             # XXX schema
78 18         23 my $argv;
79 18 50       38 if ($args{argv}) {
80 18 50       50 ref($args{argv}) eq 'ARRAY' or return [400, "argv is not an array"];
81 18         24 $argv = $args{argv};
82             } else {
83 0         0 $argv = \@ARGV;
84             }
85 18 50       36 my $spec = $args{spec} or return [400, "Please specify spec"];
86 18 50       40 ref($args{spec}) eq 'HASH' or return [400, "spec is not a hash"];
87 18         42 for (keys %$spec) {
88             return [400, "spec->{$_} is not a coderef"]
89 27 50       68 unless ref($spec->{$_}) eq 'CODE';
90             }
91              
92 18         22 my %spec_by_opt_name;
93 18         31 for (keys %$spec) {
94 27         29 my $orig = $_;
95 27         78 s/=[fios]\@?\z//;
96 27         50 s/\|.+//;
97 27         60 $spec_by_opt_name{$_} = $orig;
98             }
99              
100             my $code_find_opt = sub {
101 25     25   34 my ($wanted, $short_mode) = @_;
102 25         25 my @candidates;
103             OPT_SPEC:
104 25         49 for my $speckey (keys %$spec) {
105 36         88 $speckey =~ s/=[fios]\@?\z//;
106 36         74 my @opts = split /\|/, $speckey;
107 36         50 for my $o (@opts) {
108 46 100 100     130 next if $short_mode && length($o) > 1;
109 36 100       96 if ($o eq $wanted) {
    100          
110             # perfect match, we immediately go with this one
111 19         31 @candidates = ($opts[0]);
112 19         37 last OPT_SPEC;
113             } elsif (index($o, $wanted) == 0) {
114             # prefix match, collect candidates first
115 3         7 push @candidates, $opts[0];
116 3         5 next OPT_SPEC;
117             }
118             }
119             }
120 25 100       72 if (!@candidates) {
    100          
121 4         18 return [404, "Unknown option '$wanted'", undef,
122             {'func.unknown_opt' => $wanted}];
123             } elsif (@candidates > 1) {
124 1         9 return [300, "Option '$wanted' is ambiguous", undef, {
125             'func.ambiguous_opt' => $wanted,
126             'func.ambiguous_candidates' => [sort @candidates],
127             }];
128             }
129 20         58 return [200, "OK", $candidates[0]];
130 18         67 };
131              
132             my $code_set_val = sub {
133 17     17   29 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       52 $handler->(
140             name => $name,
141             value => (@_ ? $_[0] : 1),
142             );
143             };
144 17 100       119 if ($@) {
145 1         6 return [400, "Invalid value for option '$name': $@", undef,
146             {'func.val_invalid_opt' => $name}];
147             } else {
148 16         35 return [200];
149             }
150 18         46 };
151              
152 18         26 my %unknown_opts;
153             my %ambiguous_opts;
154 0         0 my %val_missing_opts;
155 0         0 my %val_invalid_opts;
156              
157 18         21 my $i = -1;
158 18         16 my @remaining;
159             ELEM:
160 18         72 while (++$i < @$argv) {
161 26 100       132 if ($argv->[$i] eq '--') {
    100          
    100          
162              
163 2         4 push @remaining, @{$argv}[$i+1 .. @$argv-1];
  2         5  
164 2         4 last ELEM;
165              
166             } elsif ($argv->[$i] =~ /\A--(.+?)(?:=(.*))?\z/) {
167              
168 18         40 my ($used_name, $val_in_opt) = ($1, $2);
169 18         35 my $findres = $code_find_opt->($used_name);
170 18 100       61 if ($findres->[0] == 404) { # unknown opt
    100          
    50          
171 4         7 push @remaining, $argv->[$i];
172 4         23 $unknown_opts{ $findres->[3]{'func.unknown_opt'} }++;
173 4         17 next ELEM;
174             } elsif ($findres->[0] == 300) { # ambiguous
175             $ambiguous_opts{ $findres->[3]{'func.ambiguous_opt'} } =
176 1         3 $findres->[3]{'func.ambiguous_candidates'};
177 1         5 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         17 my $opt = $findres->[2];
184              
185 13         18 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       26 if (defined $val_in_opt) {
189             # argument is taken after =
190 2 100       6 if (length $val_in_opt) {
191 1         2 my $setres = $code_set_val->($opt, $val_in_opt);
192 1 50       6 $val_invalid_opts{$opt} = $setres->[1]
193             unless $setres->[0] == 200;
194             } else {
195 1         2 $val_missing_opts{$used_name}++;
196 1         5 next ELEM;
197             }
198             } else {
199 11 100       22 if ($i+1 >= @$argv) {
200             # we are the last element
201 1         2 $val_missing_opts{$used_name}++;
202 1         3 last ELEM;
203             }
204 10         8 $i++;
205 10         19 my $setres = $code_set_val->($opt, $argv->[$i]);
206 10 100       48 $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         8 my $str = $1;
218             SHORT_OPT:
219 4         19 while ($str =~ s/(.)//) {
220 7         12 my $used_name = $1;
221 7         13 my $findres = $code_find_opt->($1, 'short');
222 7 50       16 next SHORT_OPT unless $findres->[0] == 200;
223 7         11 my $opt = $findres->[2];
224              
225 7         11 my $speckey = $spec_by_opt_name{$opt};
226             # check whether option requires an argument
227 7 100       19 if ($speckey =~ /=[fios]\@?\z/) {
228 4 100       7 if (length $str) {
229             # argument is taken from $str
230 2         5 my $setres = $code_set_val->($opt, $str);
231 2 50       6 $val_invalid_opts{$opt} = $setres->[1]
232             unless $setres->[0] == 200;
233 2         8 next ELEM;
234             } else {
235 2 100       7 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       7 $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       21 $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         17 my ($status, $msg);
263 18 100 100     120 if (!keys(%unknown_opts) && !keys(%ambiguous_opts) &&
264             !keys(%val_missing_opts) && !keys(%val_invalid_opts)) {
265 10         13 $status = 200;
266 10         14 $msg = "OK";
267             } else {
268 8         9 $status = 500;
269 8         8 my @errs;
270 8 100       18 if (keys %unknown_opts) {
271             push @errs, "Unknown option" .
272             (keys(%unknown_opts) > 1 ? "s ":" ") .
273 4 50       14 join(", ", map {"'$_'"} sort keys %unknown_opts);
  4         12  
274             }
275 8         20 for (sort keys %ambiguous_opts) {
276             push @errs, "Ambiguous option '$_' (" .
277 1         3 join("/", @{$ambiguous_opts{$_}}) . "?)";
  1         4  
278             }
279 8 100       18 if (keys %val_missing_opts) {
280             push @errs, "Missing required value for option" .
281             (keys(%val_missing_opts) > 1 ? "s ":" ") .
282 3 50       12 join(", ", map {"'$_'"} sort keys %val_missing_opts);
  3         11  
283             }
284 8         17 for (keys %val_invalid_opts) {
285             push @errs, "Invalid value for option '$_': " .
286 1         4 $val_invalid_opts{$_};
287             }
288 8 100       26 $msg = (@errs > 1 ? "Errors in parsing command-line options: " : "").
289             join("; ", @errs);
290             }
291 18 100       304 [$status, $msg, undef, {
    100          
    100          
    100          
292             'func.remaining_argv' => \@remaining,
293             ('func.unknown_opts' => \%unknown_opts )
294             x (keys(%unknown_opts) ? 1:0),
295             ('func.ambiguous_opts' => \%ambiguous_opts )
296             x (keys(%ambiguous_opts) ? 1:0),
297             ('func.val_missing_opts' => \%val_missing_opts)
298             x (keys(%val_missing_opts) ? 1:0),
299             ('func.val_invalid_opts' => \%val_invalid_opts)
300             x (keys(%val_invalid_opts) ? 1:0),
301             }];
302             }
303              
304             1;
305             # ABSTRACT: Parse command-line options
306              
307             __END__