File Coverage

blib/lib/Getopt/Yath/Instance.pm
Criterion Covered Total %
statement 233 331 70.3
branch 94 166 56.6
condition 55 124 44.3
subroutine 16 24 66.6
pod 0 12 0.0
total 398 657 60.5


line stmt bran cond sub pod time code
1             package Getopt::Yath::Instance;
2 1     1   5 use strict;
  1         2  
  1         30  
3 1     1   7 use warnings;
  1         1  
  1         141  
4              
5             our $VERSION = '2.000007';
6              
7 1     1   6 use Carp qw/croak/;
  1         1  
  1         49  
8              
9 1     1   4 use Getopt::Yath::Util qw/mod2file/;
  1         2  
  1         4  
10              
11 1     1   529 use Getopt::Yath::Option;
  1         6  
  1         53  
12 1     1   554 use Getopt::Yath::Settings;
  1         3  
  1         29  
13 1     1   5 use Getopt::Yath::Term qw/USE_COLOR color/;
  1         1  
  1         7  
14              
15 1         6 use Getopt::Yath::HashBase qw{
16            
17            
18            
19              
20            
21              
22             +dedup
23              
24             +options_groups_cache
25             +options_map_cache
26             +cache_key
27              
28             category_sort_map
29 1     1   39 };
  1         1  
30              
31             sub init {
32 1     1 0 2 my $self = shift;
33              
34 1   50     9 $self->{+OPTIONS} //= []; # List of option instances
35 1   50     4 $self->{+POSTS} //= {}; # weight => {...}
36 1   50     5 $self->{+INCLUDED} //= {}; # type => [$inst],
37              
38 1   50     6 $self->{+CATEGORY_SORT_MAP} //= {'NO CATEGORY - FIX ME' => 99999};
39              
40 1         11 $self->{+DEDUP} = {};
41             }
42              
43             sub add_option {
44 0     0 0 0 my $self = shift;
45 0         0 my $option = Getopt::Yath::Option->create(trace => [caller()], @_);
46 0         0 return $self->_option($option);
47             }
48              
49             sub add_post_process {
50 0     0 0 0 my $self = shift;
51 0         0 return $self->_post([caller()], @_);
52             }
53              
54             sub _post {
55 4     4   5 my $self = shift;
56 4         8 my ($caller, $weight, $applicable, $cb) = @_;
57              
58 4   50     8 $weight //= 0;
59              
60 4 50       16 return if $self->{+DEDUP}->{$cb}++;
61 4         5 push @{$self->{+POSTS}->{$weight}} => {caller => $caller, weight => $weight, applicable => $applicable, callback => $cb};
  4         24  
62             }
63              
64             sub include {
65 0     0 0 0 my $self = shift;
66 0         0 my ($other, $list) = @_;
67              
68 0 0       0 return unless $other;
69 0 0       0 return if $self->{+DEDUP}->{$other}++;
70              
71 0   0     0 push @{$self->included->{ref($other)} //= []} => $other;
  0         0  
72              
73 0 0       0 if (my $other_include = $other->included) {
74 0         0 for my $key (keys %{$other_include}) {
  0         0  
75 0   0     0 push @{$self->included->{$key}} => @{$other_include->{$key} // []};
  0         0  
  0         0  
76             }
77             }
78              
79 0 0       0 if ($list) {
80 0         0 my %want = map {$_ => 1} @$list;
  0         0  
81 0 0 0     0 $self->_option($_) for grep { $want{$_->title} || $want{$_->field} || $want{$_->name} } @{$other->options};
  0         0  
  0         0  
82             }
83             else {
84 0         0 $self->_option($_) for @{$other->options};
  0         0  
85             }
86              
87 0         0 for my $set (values %{$other->posts}) {
  0         0  
88 0         0 for my $post (@$set) {
89 0         0 $self->_post(@{$post}{qw/caller weight applicable callback/});
  0         0  
90             }
91             }
92              
93 0         0 $self->clear_cache;
94             }
95              
96             sub _option {
97 15     15   25 my $self = shift;
98 15         34 my ($option) = @_;
99              
100 15   50     43 my $options = $self->{+OPTIONS} //= []; # List of option instances
101              
102 15 50       70 return if $self->{+DEDUP}->{$option}++;
103 15         26 push @{$options} => $option;
  15         50  
104             }
105              
106             sub clear_cache {
107 0     0 0 0 my $self = shift;
108              
109 0         0 delete $self->{+OPTIONS_GROUPS_CACHE};
110 0         0 delete $self->{+OPTIONS_MAP_CACHE};
111 0         0 delete $self->{+CACHE_KEY};
112             }
113              
114             sub check_cache {
115 0     0 0 0 my $self = shift;
116              
117 0         0 my $options = $self->options;
118 0         0 my $new_key = @$options;
119 0   0     0 my $old_key = $self->{+CACHE_KEY} //= 0;
120              
121 0 0       0 return 1 if $old_key == $new_key;
122              
123 0         0 $self->clear_cache();
124              
125 0         0 $self->{+CACHE_KEY} = $new_key;
126              
127 0         0 return 0;
128             }
129              
130             sub have_group {
131 0     0 0 0 my $self = shift;
132 0         0 my ($name) = @_;
133 0 0       0 return 1 if $self->option_groups->{$name};
134 0         0 return 0;
135             }
136              
137             sub option_groups {
138 0     0 0 0 my $self = shift;
139 0         0 my ($in_options) = @_;
140              
141 0         0 my $options;
142 0 0       0 if ($in_options) {
143 0         0 $options = $in_options;
144             }
145             else {
146 0         0 $options = $self->options;
147              
148             return $self->{+OPTIONS_GROUPS_CACHE}
149 0 0 0     0 if $self->{+OPTIONS_GROUPS_CACHE}
150             && $self->check_cache();
151             }
152              
153 0         0 my $groups = { map {($_->group() => 1)} @$options };
  0         0  
154              
155 0 0       0 return $groups if $in_options;
156 0         0 return $self->{+OPTIONS_GROUPS_CACHE} = $groups;
157             }
158              
159             sub option_map {
160 64     64 0 82 my $self = shift;
161 64         85 my ($in_options) = @_;
162              
163 64         67 my $options;
164 64 50       110 if ($in_options) {
165 64         70 $options = $in_options;
166             }
167             else {
168 0         0 $options = $self->options;
169              
170             return $self->{+OPTIONS_MAP_CACHE}
171 0 0 0     0 if $self->{+OPTIONS_MAP_CACHE}
172             && $self->check_cache();
173             }
174              
175 64         147 my $map = {
176             custom_match => [],
177             # --whatever => $option
178             };
179              
180 64         113 for my $option (@$options) {
181 452 50       1111 push @{$map->{custom_match}} => $option->custom_matches
  0         0  
182             if $option->can('custom_matches');
183              
184 452         486 for my $form (keys %{$option->forms}) {
  452         704  
185 1435 50       2153 if (my $existing = $map->{$form}) {
186 0 0       0 croak "Option form '$form' defined twice, first in '" . $existing->trace_string . "' and again in '" . $option->trace_string . "'" if $existing ne $option;
187 0         0 next;
188             }
189              
190 1435         2414 $map->{$form} = $option;
191             }
192             }
193              
194 64 50       136 return $map if $in_options;
195 0         0 return $self->{+OPTIONS_MAP_CACHE} = $map;
196             }
197              
198             sub process_args {
199 64     64 0 99 my $self = shift;
200 64         118 my ($args, %params) = @_;
201              
202 64 50 33     275 croak "Must provide an argv arrayref" unless $args && ref($args) eq 'ARRAY';
203              
204 64         114 my $argv = [@$args]; # Make a copy
205              
206 64   33     307 my $settings = $params{settings} // Getopt::Yath::Settings->new({});
207 64   100     189 my $stops = $params{stops} // [];
208 64   50     190 my $groups = $params{groups} // {};
209 64 50 33     256 $stops = { map { ($_ => 1) } @$stops } if $stops && ref($stops) eq 'ARRAY';
  2         9  
210              
211 64   50     146 my $options = [ grep { $_->is_applicable($self, $settings) } @{$self->options // []} ];
  475         933  
  64         222  
212              
213 64         90 my @skip;
214             my $state = {
215             settings => $settings,
216             skipped => \@skip,
217             remains => $argv,
218             env => $params{env} // {},
219             cleared => $params{cleared} // {},
220             modules => $params{modules} // {},
221 64   50     623 stop => undef,
      50        
      50        
222             };
223              
224 64         133 for my $opt (@$options) {
225 475         1209 my $group = $settings->group($opt->group, 1);
226 475         1036 my $ref = $group->option_ref($opt->field, 1);
227 475 50       550 unless(defined ${$ref}) {
  475         753  
228 475         923 my $val = $opt->get_initial_value($settings);
229 475         699 my $rt = ref($val);
230 475 100       748 if (!defined($val)) {
    100          
231 329         371 $val = [];
232             }
233             elsif ($rt) {
234 86 100       206 $val = [ $rt eq 'ARRAY' ? @$val : %$val ];
235             }
236             else {
237 60         95 $val = [$val];
238             }
239 475         1118 $opt->trigger(action => 'initialize', ref => $ref, val => $val, state => $state, options => $self, settings => $settings, group => $group);
240 475         1034 $opt->add_value($ref, @$val);
241             }
242              
243 475         874 $opt->init_settings($state, $settings, $group, $ref);
244             }
245              
246 64   33 3   427 my $invalid = $params{invalid_opt_callback} // sub { die "'$_[0]' is not a valid option.\n" };
  3         86  
247              
248 64         84 my $parse_group;
249             $parse_group = sub {
250 0     0   0 my $end = shift;
251              
252 0         0 my $group = [];
253 0         0 while (@$argv) {
254 0         0 my $arg = shift(@$argv);
255 0 0       0 return $group if $arg eq $end;
256              
257 0 0       0 if (my $nest = $groups->{$arg}) {
258 0         0 $arg = $parse_group->($nest);
259             }
260              
261 0         0 push @$group => $arg;
262             }
263              
264 0         0 die "Could not find end token '$end' before end of arguments.\n";
265 64         253 };
266              
267 64         123 while (@$argv) {
268 64         134 my $map = $self->option_map($options);
269 64         113 my $base = shift @$argv;
270              
271 64 50       133 if (my $end = $groups->{$base}) {
272 0         0 push @skip => $parse_group->($end);
273 0         0 next;
274             }
275              
276 64 100       111 if ($stops->{$base}) {
277 1         5 $state->{stop} = $base;
278 1         10 last;
279             }
280              
281 63 100       198 if ($base !~ m/^-/) {
282 3 50       23 if ($params{stop_at_non_opts}) {
283 0         0 $state->{stop} = $base;
284 0         0 last;
285             }
286              
287 3 100       8 if ($params{skip_non_opts}) {
288 1         3 push @skip => $base;
289 1         5 next;
290             }
291              
292 2         5 $invalid->($base);
293             }
294              
295 60         81 my ($first, $set, $arg, $opt, $delta);
296              
297 60 100       227 if ($base =~ m/^(-[^-])(=?)(.*)$/) {
298 33         41 my ($other, $eq);
299 33         127 ($first, $set, $other) = ($1, $2, $3);
300              
301 33 50       79 if ($opt = $map->{$first}) {
302 33 100 100     96 if ($opt->allows_shortval && ($set || $other)) {
    100 100        
303 10         14 $set = 1;
304 10         20 $arg = $other;
305             }
306             elsif ($set) {
307 4         5 $arg = $other;
308             }
309             else {
310 19 100       59 unshift @$argv => "-$other" if $other;
311             }
312             }
313             }
314             else {
315 27         97 ($first, $set, $arg) = split(/(=)/, $base, 2);
316 27         55 $opt = $map->{$first};
317             }
318              
319 60 100       98 unless ($opt) {
320 2 50       22 if (my $list = $map->{custom_match}) {
321 2         6 for my $match (@$list) {
322 0         0 ($opt, $delta, $arg) = $match->($base, $state);
323 0 0       0 next unless $opt;
324 0         0 $set = 1;
325 0         0 last;
326             }
327             }
328             }
329              
330 60 50 66     135 die "Use of 'arg=val' form without a value is not valid in option '$base'.\n"
331             if $set && !defined($arg);
332              
333 60 100       120 unless ($opt) {
334 2 100       5 if ($params{skip_invalid_opts}) {
335 1         2 push @skip => $base;
336 1         4 next;
337             }
338              
339 1 50       2 if ($params{stop_at_invalid_opts}) {
340 0         0 $state->{stop} = $base;
341 0         0 last;
342             }
343              
344 1         10 $invalid->($base);
345             }
346              
347 58 100 100     105 die "Use of 'arg=val' form is not allowed in option '$base'. Arguments are not allowed for this option type.\n"
348             if $set && !$opt->allows_arg;
349              
350 57   33     157 $delta //= $opt->forms->{$first};
351              
352 57 100       159 $state->{modules}->{$opt->module}++ unless $opt->no_module;
353              
354 57         93 my $group_name = $opt->group;
355 57         90 my $field_name = $opt->field;
356 57         133 my $group = $settings->group($group_name, 1);
357 57         112 my $ref = $group->option_ref($field_name, 1);
358              
359 57 100       105 if ($delta < 0) {
360 12         85 $opt->clear_field($ref);
361 12         39 $opt->trigger(action => 'clear', ref => $ref, val => undef, state => $state, options => $self, settings => $settings, group => $group);
362 12         31 $state->{cleared}->{$group_name}->{$field_name} = 1;
363 12 50       71 next unless $set;
364             }
365              
366 45 100       104 delete $state->{cleared}->{$group_name}->{$field_name} if $state->{cleared}->{$group_name};
367              
368 45 100 100     104 if ($opt->requires_arg && !$set) {
369 9 100       31 die "No argument provided to '$first'.\n" unless @$argv;
370 8         12 $arg = shift(@$argv);
371             }
372              
373 44 100       78 if ($arg) {
374 23 50       51 if (my $end = $groups->{$arg}) {
375 0         0 $arg = $parse_group->($end);
376             }
377             }
378              
379 44 0 33     84 if (ref($arg) && @$arg > 1 && !$opt->allows_list) {
      33        
380 0         0 die "Option '$first' cannot take multiple values, got: [" . join(', ' => @$arg) . "].\n";
381             }
382              
383 44         59 my $from = '';
384 44         52 my @val;
385 44 100       90 if (defined $arg) {
    100          
386 24         31 $from = 'arg';
387 24 50       91 @val = $opt->normalize_value(ref($arg) ? @$arg : $arg);
388             }
389             elsif ($opt->allows_autofill) {
390 7         14 $from = 'autofill';
391 7         31 @val = $opt->get_autofill_value($settings);
392             }
393             else {
394 13         17 $from = 'no_arg';
395 13         29 @val = $opt->no_arg_value($settings);
396             }
397              
398 44 50       141 if ($opt->mod_adds_options) {
399 0         0 my ($class) = @val;
400 0         0 require(mod2file($class));
401 0 0       0 if ($class->can('options')) {
402 0 0       0 if (my $add = $class->options) {
403 0         0 $self->include($add);
404             }
405             }
406             }
407              
408 44         185 $opt->trigger(action => 'set', ref => $ref, val => \@val, state => $state, options => $self, settings => $settings, group => $group, set_from => $from);
409 44         118 my @bad = $opt->check_value(\@val);
410 44 50       73 if (@bad) {
411 0 0       0 die "Invalid value(s) for option '$first': " . join(', ' => map {defined($_) ? "'$_'" : 'undef' } @bad) . "\n";
  0         0  
412             }
413 44         86 $opt->add_value($ref, @val);
414             }
415              
416 59         118 for my $opt (@$options) {
417 447         725 my $group_name = $opt->group;
418 447         624 my $field_name = $opt->field;
419 447         785 my $group = $settings->group($group_name, 1);
420 447         745 my $ref = $group->option_ref($field_name, 1);
421              
422             # Do not set the default if the --no-OPT form was used.
423 447 100 66     1288 next if $state->{cleared} && $state->{cleared}->{$group_name} && $state->{cleared}->{$group_name}->{$field_name};
      100        
424 437 100       9002 next if $opt->is_populated($ref);
425 354         663 $opt->add_value($ref, $opt->get_default_value($settings));
426             }
427              
428 59 50       124 unless ($params{skip_posts}) {
429 59         82 for my $weight (sort { $a <=> $b } keys %{$self->{+POSTS}}) {
  6         23  
  59         188  
430 9         26 for my $set (@{$self->{+POSTS}->{$weight}}) {
  9         20  
431 12 50 33     32 next if $set->{applicable} && !$set->{applicable}->($set, $self, $settings);
432 12         26 $set->{callback}->($self, $state);
433             }
434             }
435             }
436              
437 59         3605 for my $opt (@$options) {
438 447         926 my $group = $settings->group($opt->group, 1);
439 447         865 my $ref = $group->option_ref($opt->field, 1);
440              
441 447   100     492 for my $env (@{$opt->clear_env_vars // []}) {
  447         1236  
442 12         30 $state->{env}->{$env} = undef;
443 12 50       87 delete $ENV{$env} unless $params{no_set_env};
444             }
445              
446 447         1012 $opt->finalize_settings($state, $settings, $group, $ref);
447              
448 447 100       792 next unless $opt->can_set_env;
449              
450 367 100       721 my $to_set = $opt->set_env_vars or next;
451 12 50       25 next unless @$to_set;
452              
453 12 100       1841 next unless $opt->is_populated($ref);
454              
455 9         20 for my $name (@$to_set) {
456 9         18 my $env = "$name";
457 9         29 $env =~ s/^(!)//;
458 9         21 my $neg = $1;
459 9 50       26 my @val = $opt->get_env_value($env => $ref) or next;
460 9 50       24 if (@val > 1) {
461 0         0 my $title = $opt->title;
462 0   0     0 my $trace = $opt->trace // ['', 'unknown', 'n/a'];
463 0         0 die "Option '$title' defined in $trace->[1] line $trace->[2] returned more than one value when get_env_value($env) was called.\n";
464             }
465              
466 9         16 my $setval = $val[0];
467 9 100       22 $setval = $setval ? 0 : 1 if $neg;
    100          
468              
469 9         25 $state->{env}->{$env} = $val[0];
470 9 50       98 $ENV{$env} = $val[0] unless $params{no_set_env};
471             }
472             }
473              
474 59         706 return $state;
475             }
476              
477             my %DOC_FORMATS = (
478             'cli' => [
479             'cli_docs', # Method to call on opt
480             "\n", # how to join lines
481             sub { $_[4] ? "\n" . color('bold underline white') . $_[1] . color('reset') . " ($_[3])" : "\n$_[1] ($_[3])" }, # how to render the category
482             sub { $_[0] =~ s/^/ /mg; "$_[0]\n" }, # transform the value from the opt
483             sub { }, # add this at the end
484             ],
485             'pod' => [
486             'pod_docs', # Method to call on opt
487             "\n\n", # how to join lines
488             sub { ($_[0] ? ("=back") : (), "=head$_[2] $_[1]", "=over 4") }, # how to render the category
489             sub { $_[0] }, # transform the value from the opt
490             sub { $_[0] ? ("=back\n") : () }, # add this at the end
491             ],
492             );
493              
494             sub docs {
495 2     2 0 4 my $self = shift;
496 2         8 my ($format, %params) = @_;
497              
498 2   33     20 $params{color} //= USE_COLOR() && -t STDOUT;
499              
500 2         4 my $settings = $params{settings};
501 2 50 50     4 my $opts = [ grep { $params{applicable} || $_->is_applicable($self, $settings) } @{$self->options // []} ];
  28         60  
  2         9  
502              
503 2   50     4 $format //= "UNDEFINED";
504 2 50       8 my $fset = $DOC_FORMATS{$format} or croak "Invalid documentation format '$format'";
505 2         6 my ($fmeth, $join, $fcat, $ftrans, $fend) = @$fset;
506              
507 2 50       6 return unless $opts;
508 2 50       4 return unless @$opts;
509              
510 2         6 my @render = @$opts;
511              
512 2 50       5 @render = grep { $_->group eq $params{group} } @render if $params{group};
  0         0  
513              
514 2 50       4 return "\n\n!! Invalid option group: $params{group} !!"
515             unless @render;
516              
517 2         8 @render = sort { $self->doc_sort_ops($a, $b) } @render;
  74         98  
518              
519 2         4 my @out;
520              
521             my $cat;
522 2         5 for my $opt (@render) {
523 28 100 66     87 if (!$cat || $opt->category ne $cat) {
524 2         13 push @out => $fcat->($cat, $opt->category, $params{head}, $opt->group, $params{color});
525 2         7 $cat = $opt->category;
526             }
527              
528 28         95 my $help = $opt->$fmeth(%params);
529 28         50 push @out => $ftrans->($help);
530             }
531              
532 2         7 push @out => $fend->($cat);
533 2         106 s/[ \t]+$//gm for @out;
534              
535 2         42 return join $join => @out;
536             }
537              
538             sub doc_sort_ops {
539 74     74 0 60 my $self = shift;
540 74         81 my ($a, $b, %params) = @_;
541              
542 74         77 my $map = $self->{+CATEGORY_SORT_MAP};
543 74   50     172 my $aw = $map->{$a->category} || 0;
544 74   50     161 my $bw = $map->{$b->category} || 0;
545              
546 74         73 my $ret = $aw <=> $bw;
547 74 50       81 if ($params{group_first}) {
548 0   0     0 $ret ||= $a->group cmp $b->group;
549 0   0     0 $ret ||= $a->category cmp $b->category;
550             }
551             else {
552 74   33     184 $ret ||= $a->category cmp $b->category;
553 74   33     153 $ret ||= $a->group cmp $b->group;
554             }
555 74   50     338 $ret ||= ($a->prefix || '') cmp ($b->prefix || '');
      50        
      33        
556 74   33     220 $ret ||= $a->name cmp $b->name;
557              
558 74         103 return $ret;
559             }
560              
561             1;
562              
563             __END__