blib/lib/Getopt/EX/Loader.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 150 | 194 | 77.3 |
branch | 48 | 84 | 57.1 |
condition | 2 | 13 | 15.3 |
subroutine | 28 | 34 | 82.3 |
pod | 4 | 19 | 21.0 |
total | 232 | 344 | 67.4 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Getopt::EX::Loader; | ||||||
2 | 6 | 6 | 162306 | use version; our $VERSION = version->declare("2.1.4"); | |||
6 | 4257 | ||||||
6 | 35 | ||||||
3 | |||||||
4 | 6 | 6 | 582 | use v5.14; | |||
6 | 28 | ||||||
5 | 6 | 6 | 32 | use warnings; | |||
6 | 10 | ||||||
6 | 180 | ||||||
6 | 6 | 6 | 36 | use utf8; | |||
6 | 9 | ||||||
6 | 64 | ||||||
7 | 6 | 6 | 151 | use Carp; | |||
6 | 11 | ||||||
6 | 351 | ||||||
8 | |||||||
9 | 6 | 6 | 37 | use Exporter 'import'; | |||
6 | 9 | ||||||
6 | 474 | ||||||
10 | our @EXPORT = qw(); | ||||||
11 | our %EXPORT_TAGS = ( ); | ||||||
12 | our @EXPORT_OK = qw(); | ||||||
13 | |||||||
14 | 6 | 6 | 37 | use Data::Dumper; | |||
6 | 20 | ||||||
6 | 385 | ||||||
15 | 6 | 6 | 42 | use List::Util qw(pairmap); | |||
6 | 10 | ||||||
6 | 553 | ||||||
16 | |||||||
17 | 6 | 6 | 2523 | use Getopt::EX::Module; | |||
6 | 13 | ||||||
6 | 274 | ||||||
18 | 6 | 6 | 36 | use Getopt::EX::Func qw(parse_func); | |||
6 | 23 | ||||||
6 | 269 | ||||||
19 | 6 | 6 | 2439 | use Getopt::EX::Colormap qw(colorize); | |||
6 | 13 | ||||||
6 | 7222 | ||||||
20 | |||||||
21 | our $debug = 0; | ||||||
22 | |||||||
23 | sub new { | ||||||
24 | 9 | 9 | 0 | 3665 | my $class = shift; | ||
25 | |||||||
26 | 9 | 72 | my $obj = bless { | ||||
27 | BUCKETS => [], | ||||||
28 | BASECLASS => undef, | ||||||
29 | MODULE_OPT => '-M', | ||||||
30 | DEFAULT => 'default', | ||||||
31 | PARSE_MODULE_OPT => 1, | ||||||
32 | IGNORE_NO_MODULE => 0, | ||||||
33 | }, $class; | ||||||
34 | |||||||
35 | 9 | 50 | 53 | configure $obj @_ if @_; | |||
36 | |||||||
37 | 9 | 26 | $obj; | ||||
38 | } | ||||||
39 | |||||||
40 | our @OPTIONS = qw( | ||||||
41 | RCFILE | ||||||
42 | BASECLASS | ||||||
43 | MODULE_OPT | ||||||
44 | DEFAULT | ||||||
45 | PARSE_MODULE_OPT | ||||||
46 | IGNORE_NO_MODULE | ||||||
47 | ); | ||||||
48 | |||||||
49 | sub configure { | ||||||
50 | 9 | 9 | 1 | 20 | my $obj = shift; | ||
51 | 9 | 30 | my %opt = @_; | ||||
52 | |||||||
53 | 9 | 25 | for my $opt (@OPTIONS) { | ||||
54 | 54 | 100 | 109 | next if $opt eq 'RCFILE'; | |||
55 | 45 | 100 | 108 | if (exists $opt{$opt}) { | |||
56 | 9 | 49 | $obj->{$opt} = delete $opt{$opt}; | ||||
57 | } | ||||||
58 | } | ||||||
59 | |||||||
60 | 9 | 100 | 34 | if (my $rc = delete $opt{RCFILE}) { | |||
61 | 1 | 50 | 5 | my @rc = ref $rc eq 'ARRAY' ? @$rc : $rc; | |||
62 | 1 | 3 | for (@rc) { | ||||
63 | 1 | 4 | $obj->load(FILE => $_); | ||||
64 | } | ||||||
65 | } | ||||||
66 | |||||||
67 | 9 | 50 | 22 | warn "Unknown option: ", Dumper \%opt if %opt; | |||
68 | |||||||
69 | 9 | 21 | $obj; | ||||
70 | } | ||||||
71 | |||||||
72 | sub baseclass { | ||||||
73 | 22 | 22 | 0 | 64 | my $obj = shift; | ||
74 | @_ ? $obj->{BASECLASS} = shift | ||||||
75 | 22 | 50 | 127 | : $obj->{BASECLASS}; | |||
76 | } | ||||||
77 | |||||||
78 | sub buckets { | ||||||
79 | 95 | 95 | 1 | 145 | my $obj = shift; | ||
80 | 95 | 127 | @{ $obj->{BUCKETS} }; | ||||
95 | 216 | ||||||
81 | } | ||||||
82 | |||||||
83 | sub append { | ||||||
84 | 13 | 13 | 0 | 28 | my $obj = shift; | ||
85 | 13 | 18 | push @{ $obj->{BUCKETS} }, @_; | ||||
13 | 50 | ||||||
86 | } | ||||||
87 | |||||||
88 | sub load { | ||||||
89 | 22 | 22 | 0 | 33 | my $obj = shift; | ||
90 | 22 | 52 | my $bucket = | ||||
91 | Getopt::EX::Module->new(@_, BASECLASS => $obj->baseclass); | ||||||
92 | 13 | 72 | $obj->append($bucket); | ||||
93 | 13 | 57 | $bucket; | ||||
94 | } | ||||||
95 | |||||||
96 | sub load_file { | ||||||
97 | 0 | 0 | 1 | 0 | my $obj = shift; | ||
98 | 0 | 0 | $obj->load(FILE => shift); | ||||
99 | } | ||||||
100 | |||||||
101 | sub load_module { | ||||||
102 | 21 | 21 | 1 | 37 | my $obj = shift; | ||
103 | 21 | 88 | $obj->load(MODULE => shift); | ||||
104 | } | ||||||
105 | |||||||
106 | sub defaults { | ||||||
107 | 0 | 0 | 0 | 0 | my $obj = shift; | ||
108 | 0 | 0 | map { $_->default } $obj->buckets; | ||||
0 | 0 | ||||||
109 | } | ||||||
110 | |||||||
111 | sub calls { | ||||||
112 | 0 | 0 | 0 | 0 | my $obj = shift; | ||
113 | 0 | 0 | map { $_->call } $obj->buckets; | ||||
0 | 0 | ||||||
114 | } | ||||||
115 | |||||||
116 | sub builtins { | ||||||
117 | 3 | 3 | 0 | 7 | my $obj = shift; | ||
118 | 3 | 8 | map { $_->builtin } $obj->buckets; | ||||
4 | 20 | ||||||
119 | } | ||||||
120 | |||||||
121 | sub hashed_builtins { | ||||||
122 | 2 | 2 | 0 | 14 | my $obj = shift; | ||
123 | 2 | 3 | my $hash = shift; | ||||
124 | pairmap { | ||||||
125 | 8 | 50 | 8 | 45 | my($key) = $a =~ /^([-\w]+)/ or die; | ||
126 | 8 | 25 | $hash->{$key} = $b; | ||||
127 | 8 | 49 | $a; | ||||
128 | 2 | 12 | } $obj->builtins; | ||||
129 | } | ||||||
130 | |||||||
131 | sub deal_with { | ||||||
132 | 9 | 9 | 0 | 1759 | my $obj = shift; | ||
133 | 9 | 18 | my $argv = shift; | ||||
134 | |||||||
135 | 9 | 50 | 31 | if (my $default = $obj->{DEFAULT}) { | |||
136 | 9 | 50 | 22 | if (my $bucket = eval { $obj->load_module($default) }) { | |||
9 | 25 | ||||||
137 | 0 | 0 | $bucket->run_inits($argv); | ||||
138 | } else { | ||||||
139 | 9 | 50 | 5 | 99 | $!{ENOENT} or die $@; | ||
5 | 2518 | ||||||
5 | 7652 | ||||||
5 | 59 | ||||||
140 | } | ||||||
141 | } | ||||||
142 | 9 | 50 | 200 | $obj->modopt($argv) if $obj->{PARSE_MODULE_OPT}; | |||
143 | 9 | 44 | $obj->expand($argv); | ||||
144 | 9 | 39 | $obj; | ||||
145 | } | ||||||
146 | |||||||
147 | sub modopt { | ||||||
148 | 32 | 32 | 0 | 53 | my $obj = shift; | ||
149 | 32 | 45 | my $argv = shift; | ||||
150 | |||||||
151 | 32 | 50 | 137 | my $start = $obj->{MODULE_OPT} // return (); | |||
152 | 32 | 50 | 75 | $start eq '' and return (); | |||
153 | 32 | 268 | my $start_re = qr/\Q$start\E/; | ||||
154 | 32 | 56 | my @modules; | ||||
155 | 32 | 79 | while (@$argv) { | ||||
156 | 38 | 100 | 282 | if (my($modpart) = ($argv->[0] =~ /^$start_re(.+)/)) { | |||
157 | 9 | 34 | debug_argv($argv); | ||||
158 | 9 | 50 | 39 | if (my $mod = $obj->parseopt($modpart, $argv)) { | |||
159 | 9 | 26 | push @modules, $mod; | ||||
160 | } else { | ||||||
161 | 0 | 0 | last; | ||||
162 | } | ||||||
163 | 9 | 32 | next; | ||||
164 | } | ||||||
165 | 29 | 66 | last; | ||||
166 | } | ||||||
167 | 32 | 118 | @modules; | ||||
168 | } | ||||||
169 | |||||||
170 | sub parseopt { | ||||||
171 | 9 | 9 | 0 | 19 | my $obj = shift; | ||
172 | 9 | 44 | my($mod, $argv) = @_; | ||||
173 | 9 | 18 | my $call; | ||||
174 | |||||||
175 | ## | ||||||
176 | ## Check -Mmod::func(arg) or -Mmod::func=arg | ||||||
177 | ## | ||||||
178 | 9 | 50 | 175 | if ($mod =~ s{ | |||
179 | ^ (? |
||||||
180 | (?: | ||||||
181 | :: | ||||||
182 | (? |
||||||
183 | \w+ | ||||||
184 | (?: (? [(]) | = ) ## start with '(' or '=' |
||||||
185 | (? |
||||||
186 | (?( ) [)] | ) ## close ')' or none |
||||||
187 | ) | ||||||
188 | )? | ||||||
189 | $ | ||||||
190 | }{$+{name}}x) { | ||||||
191 | 9 | 54 | $call = $+{call}; | ||||
192 | } | ||||||
193 | |||||||
194 | 9 | 50 | 32 | my $bucket = eval { $obj->load_module($mod) } or do { | |||
9 | 30 | ||||||
195 | 0 | 0 | 0 | if ($!{ENOENT}) { | |||
196 | 0 | 0 | 0 | 0 | if ($obj->{IGNORE_NO_MODULE} and $@ =~ /need to install the (\w+::)*$mod/) { | ||
197 | 0 | 0 | return undef; | ||||
198 | } else { | ||||||
199 | 0 | 0 | die "Can't load module \"$mod\".\n"; | ||||
200 | } | ||||||
201 | } else { | ||||||
202 | 0 | 0 | die $@; | ||||
203 | } | ||||||
204 | }; | ||||||
205 | |||||||
206 | 9 | 19 | shift @$argv; | ||||
207 | |||||||
208 | 9 | 50 | 32 | if ($call) { | |||
209 | 0 | 0 | $bucket->call(join '::', $bucket->module, $call); | ||||
210 | } | ||||||
211 | |||||||
212 | ## | ||||||
213 | ## If &getopt is defined in module, call it and replace @ARGV. | ||||||
214 | ## | ||||||
215 | 9 | 36 | $bucket->run_inits($argv); | ||||
216 | |||||||
217 | 9 | 37 | $bucket; | ||||
218 | } | ||||||
219 | |||||||
220 | sub expand { | ||||||
221 | 9 | 9 | 0 | 25 | my $obj = shift; | ||
222 | 9 | 20 | my $argv = shift; | ||||
223 | |||||||
224 | ## | ||||||
225 | ## Insert module defaults. | ||||||
226 | ## | ||||||
227 | unshift @$argv, map { | ||||||
228 | 9 | 100 | 105 | if (my @s = $_->default()) { | |||
16 | 70 | ||||||
229 | 5 | 27 | my @modules = $obj->modopt(\@s); | ||||
230 | 5 | 36 | [ @s, map { $_->default } @modules ]; | ||||
0 | 0 | ||||||
231 | } else { | ||||||
232 | 11 | 19 | (); | ||||
233 | } | ||||||
234 | } $obj->buckets; | ||||||
235 | |||||||
236 | ## | ||||||
237 | ## Expand user defined option. | ||||||
238 | ## | ||||||
239 | ARGV: | ||||||
240 | 9 | 51 | for (my $i = 0; $i < @$argv; $i++) { | ||||
241 | |||||||
242 | 83 | 50 | 190 | last if $argv->[$i] eq '--'; | |||
243 | 83 | 117 | my $current = $argv->[$i]; | ||||
244 | |||||||
245 | 83 | 156 | for my $bucket ($obj->buckets) { | ||||
246 | |||||||
247 | 96 | 125 | my @s; | ||||
248 | 96 | 100 | 194 | if (ref $current eq 'ARRAY') { | |||
249 | ## | ||||||
250 | ## Expand defaults. | ||||||
251 | ## | ||||||
252 | 5 | 18 | @s = @$current; | ||||
253 | 5 | 10 | $current = 'DEFAULT'; | ||||
254 | } | ||||||
255 | else { | ||||||
256 | ## | ||||||
257 | ## Try entire string match, and check --option=value. | ||||||
258 | ## | ||||||
259 | 91 | 189 | @s = $bucket->getopt($current); | ||||
260 | 91 | 100 | 206 | if (not @s) { | |||
261 | 78 | 100 | 280 | $current =~ /^(.+?)=(.*)/ or next; | |||
262 | 6 | 50 | 17 | @s = $bucket->getopt($1) or next; | |||
263 | 0 | 0 | splice @$argv, $i, 1, ($1, $2); | ||||
264 | } | ||||||
265 | } | ||||||
266 | |||||||
267 | 18 | 51 | my @follow = splice @$argv, $i; | ||||
268 | |||||||
269 | ## | ||||||
270 | ## $ |
||||||
271 | ## | ||||||
272 | 18 | 85 | s/\$<(-?\d+)>/$follow[$1]/ge foreach @s; | ||||
0 | 0 | ||||||
273 | |||||||
274 | 18 | 29 | shift @follow; | ||||
275 | |||||||
276 | 18 | 96 | debug_argv({color=>'R'}, $argv, undef, \@s, \@follow); | ||||
277 | |||||||
278 | ## | ||||||
279 | ## $ |
||||||
280 | ## | ||||||
281 | 18 | 37 | my $modified; | ||||
282 | @s = map sub { | ||||||
283 | 18 | 50 | 18 | 54 | $modified += s/\$ |
||
4 | 19 | ||||||
284 | 18 | 100 | 213 | m{\A \$ < # $< | |||
285 | (? |
||||||
286 | (?: \( (? |
||||||
287 | (?: ,(? |
||||||
288 | > \z # > | ||||||
289 | }x or return $_; | ||||||
290 | 5 | 8 | $modified++; | ||||
291 | 5 | 100 | 31 | return () if $+{cmd} eq 'ignore'; | |||
292 | my $p = ($+{cmd} eq 'copy') | ||||||
293 | 4 | 100 | 18 | ? do { my @new = @follow; \@new } | |||
1 | 3 | ||||||
1 | 2 | ||||||
294 | : \@follow; | ||||||
295 | my @arg = @$p == 0 ? () | ||||||
296 | : defined $+{len} ? splice @$p, $+{off}//0, $+{len} | ||||||
297 | 4 | 50 | 50 | 35 | : splice @$p, $+{off}//0; | ||
50 | 0 | ||||||
298 | 4 | 100 | 34 | ($+{cmd} eq 'remove') ? () : @arg; | |||
299 | 18 | 105 | }->(), @s; | ||||
300 | |||||||
301 | 18 | 78 | @s = $bucket->expand_args(@s); | ||||
302 | 18 | 100 | 63 | debug_argv({color=>'B'}, $argv, undef, \@s, \@follow) if $modified; | |||
303 | |||||||
304 | 18 | 36 | my(@module, @default); | ||||
305 | 18 | 50 | 66 | if (@module = $obj->modopt(\@s)) { | |||
306 | 0 | 0 | @default = grep { @$_ } map { [ $_->default ] } @module; | ||||
0 | 0 | ||||||
0 | 0 | ||||||
307 | 0 | 0 | debug_argv({color=>'Y'}, $argv, \@default, \@s, \@follow); | ||||
308 | } | ||||||
309 | 18 | 98 | push @$argv, @default, @s, @follow; | ||||
310 | |||||||
311 | 18 | 50 | 92 | redo ARGV if $i < @$argv; | |||
312 | } | ||||||
313 | } | ||||||
314 | } | ||||||
315 | |||||||
316 | sub debug_argv { | ||||||
317 | 36 | 50 | 36 | 0 | 112 | $debug or return; | |
318 | 0 | 0 | 0 | my $opt = ref $_[0] eq 'HASH' ? shift : {}; | |||
319 | 0 | 0 | my($before, $default, $working, $follow) = @_; | ||||
320 | 0 | 0 | 0 | my $color = $opt->{color} // 'R'; | |||
321 | printf STDERR | ||||||
322 | "\@ARGV = %s\n", | ||||||
323 | 0 | 0 | 0 | 0 | array_to_str(pairmap { $a ? colorize($b, array_to_str(@$a)) : () } | ||
324 | 0 | 0 | $before, "L10", | ||||
325 | $default, "$color;DI", | ||||||
326 | $working, "$color;D", | ||||||
327 | $follow, "M"); | ||||||
328 | } | ||||||
329 | |||||||
330 | sub array_to_str { | ||||||
331 | join ' ', map { | ||||||
332 | 0 | 0 | 0 | 0 | 0 | if (ref eq 'ARRAY') { | |
0 | 0 | ||||||
333 | 0 | 0 | join ' ', '[', array_to_str(@$_), ']'; | ||||
334 | } else { | ||||||
335 | 0 | 0 | $_; | ||||
336 | } | ||||||
337 | } @_; | ||||||
338 | } | ||||||
339 | |||||||
340 | sub modules { | ||||||
341 | 0 | 0 | 0 | 0 | my $obj = shift; | ||
342 | 0 | 0 | 0 | my $class = $obj->baseclass // return (); | |||
343 | 0 | 0 | 0 | my @base = ref $class eq 'ARRAY' ? @$class : ($class); | |||
344 | 0 | 0 | for (@base) { | ||||
345 | 0 | 0 | s/::/\//g; | ||||
346 | 0 | 0 | 0 | $_ = "/$_" if $_ ne ""; | |||
347 | } | ||||||
348 | |||||||
349 | map { | ||||||
350 | 0 | 0 | my $base = $_; | ||||
0 | 0 | ||||||
351 | 0 | 0 | grep { /^[a-z]/ } | ||||
352 | 0 | 0 | map { /(\w+)\.pm$/ } | ||||
353 | 0 | 0 | map { glob $_ . $base . "/*.pm" } | ||||
0 | 0 | ||||||
354 | @INC; | ||||||
355 | } @base; | ||||||
356 | } | ||||||
357 | |||||||
358 | 1; | ||||||
359 | |||||||
360 | =head1 NAME | ||||||
361 | |||||||
362 | Getopt::EX::Loader - RC/Module loader | ||||||
363 | |||||||
364 | =head1 SYNOPSIS | ||||||
365 | |||||||
366 | use Getopt::EX::Loader; | ||||||
367 | |||||||
368 | my $loader = Getopt::EX::Loader->new( | ||||||
369 | BASECLASS => 'App::example', | ||||||
370 | ); | ||||||
371 | |||||||
372 | $loader->load_file("$ENV{HOME}/.examplerc"); | ||||||
373 | |||||||
374 | $loader->deal_with(\@ARGV); | ||||||
375 | |||||||
376 | my $parser = Getopt::Long::Parser->new; | ||||||
377 | $parser->getoptions(... , $loader->builtins); | ||||||
378 | or | ||||||
379 | $parser->getoptions(\%hash, ... , $loader->hashed_builtins(\%hash)); | ||||||
380 | |||||||
381 | =head1 DESCRIPTION | ||||||
382 | |||||||
383 | This is the main interface to use L |
||||||
384 | create loader object, load user defined rc file, load modules | ||||||
385 | specified by command arguments, substitute user defined option and | ||||||
386 | insert default options defined in rc file or modules, get module | ||||||
387 | defined built-in option definition for option parser. | ||||||
388 | |||||||
389 | Most of work is done in C |
||||||
390 | arguments and load modules specified by B<-M> option by default. Then | ||||||
391 | it scans options and substitute them according to the definitions in | ||||||
392 | rc file or modules. If RC and modules defines default options, they | ||||||
393 | are inserted to the arguments. | ||||||
394 | |||||||
395 | Module can define built-in options which should be handled option | ||||||
396 | parser. They can be taken by C |
||||||
397 | them to option parser. | ||||||
398 | |||||||
399 | If option values are stored in a hash, use C |
||||||
400 | hash reference. Actually, C |
||||||
401 | the current version of B |
||||||
402 | documented. | ||||||
403 | |||||||
404 | If C |
||||||
405 | is prepended to all module names. So command line | ||||||
406 | |||||||
407 | % example -Mfoo | ||||||
408 | |||||||
409 | will load C |
||||||
410 | |||||||
411 | In this case, if module C |
||||||
412 | automatically without explicit indication. Default module can be used | ||||||
413 | just like a startup RC file. | ||||||
414 | |||||||
415 | |||||||
416 | =head1 METHODS | ||||||
417 | |||||||
418 | =over 4 | ||||||
419 | |||||||
420 | =item B |
||||||
421 | |||||||
422 | =over 4 | ||||||
423 | |||||||
424 | =item RCFILE | ||||||
425 | |||||||
426 | Define the name of startup file. | ||||||
427 | |||||||
428 | =item BASECLASS | ||||||
429 | |||||||
430 | Define the base class for user defined module. Use array reference to | ||||||
431 | specify multiple base classes; they are tried to be loaded in order. | ||||||
432 | |||||||
433 | =item MODULE_OPT | ||||||
434 | |||||||
435 | Define the module option string. String C<-M> is set by default. | ||||||
436 | |||||||
437 | =item DEFAULT | ||||||
438 | |||||||
439 | Define default module name. String C |
||||||
440 | C |
||||||
441 | |||||||
442 | =item PARSE_MODULE_OPT | ||||||
443 | |||||||
444 | Default true, and parse module options given to C |
||||||
445 | When disabled, module option in command line argument is not | ||||||
446 | processed, but module option given in rc or module files are still | ||||||
447 | effective. | ||||||
448 | |||||||
449 | =item IGNORE_NO_MODULE | ||||||
450 | |||||||
451 | Default false, and process dies when given module was not found on the | ||||||
452 | system. When set true, program ignores not-existing module and stop | ||||||
453 | parsing at the point leaving the argument untouched. | ||||||
454 | |||||||
455 | =back | ||||||
456 | |||||||
457 | =item B |
||||||
458 | |||||||
459 | Return loaded L |
||||||
460 | |||||||
461 | =item B |
||||||
462 | |||||||
463 | Load specified file. | ||||||
464 | |||||||
465 | =item B |
||||||
466 | |||||||
467 | Load specified module. | ||||||
468 | |||||||
469 | =back |