File Coverage

blib/lib/App/Easer/V2.pm
Criterion Covered Total %
statement 958 1127 85.0
branch 246 394 62.4
condition 99 175 56.5
subroutine 126 144 87.5
pod 4 4 100.0
total 1433 1844 77.7


line stmt bran cond sub pod time code
1             package App::Easer::V2;
2 27     27   321195 use v5.24;
  27         138  
3 27     27   153 use warnings;
  27         47  
  27         1559  
4 27     27   640 use experimental qw< signatures >;
  27         4882  
  27         159  
5             { our $VERSION = '2.014' }
6 27     27   5348 use Carp;
  27         77  
  27         2266  
7              
8 27     27   1207 use parent 'Exporter';
  27         696  
  27         208  
9             our @EXPORT_OK = qw< appeaser_api d dd run >;
10              
11             # repeated stuff to ease direct usage and fatpack-like inclusion
12 2     2 1 226177 sub appeaser_api { __PACKAGE__ =~ s{.*::}{}rmxs }
13 0     0 1 0 sub d { warn dd(@_) }
14              
15 0     0 1 0 sub dd (@stuff) {
  0         0  
  0         0  
16 27     27   6713 no warnings;
  27         49  
  27         13499  
17 0         0 require Data::Dumper;
18 0         0 local $Data::Dumper::Indent = 1;
19 0         0 local $Data::Dumper::Sortkeys = 1;
20 0 0 0     0 Data::Dumper::Dumper(
    0          
21             @stuff == 0 ? []
22             : (ref($stuff[0]) || @stuff % 2) ? \@stuff
23             : {@stuff}
24             );
25             } ## end sub dd (@stuff)
26              
27 120     120 1 5277539 sub run ($app, @args) {
  120         317  
  120         601  
  120         223  
28 120         253 my $class = 'App::Easer::V2::Command';
29 120 0       1477 my $instance =
    50          
30             ref($app) eq 'HASH' ? $class->new($app)
31             : ref($app) eq 'ARRAY' ? $class->instantiate($app->@*)
32             : $class->instantiate($app);
33 120         539 return $instance->run(@args);
34             } ## end sub run
35              
36 8     8   27895 sub import ($package, @args) {
  8         20  
  8         23  
  8         13  
37 8         25 my $target = caller;
38 8         43 my @args_for_exporter;
39 8         13 our %registered;
40              
41 8         18 my $parent_class = 'App::Easer::V2::Command';
42 8         49 while (@args) {
43 14         49 my $request = shift @args;
44 14 100       64 if ($request eq '-command') {
    50          
    50          
    100          
    50          
45 6         15 $registered{$target} = 1;
46 27     27   225 no strict 'refs';
  27         47  
  27         3178  
47 6         8 push @{$target . '::ISA'}, $parent_class;
  6         133  
48             }
49             elsif ($request eq '-inherit') {
50 27     27   246 no strict 'refs';
  27         87  
  27         3853  
51 0         0 push @{$target . '::ISA'}, $parent_class;
  0         0  
52             }
53             elsif ($request eq '-register') {
54 0         0 $registered{$target} = 1;
55             }
56             elsif ($request eq '-spec') {
57 4 50       11 Carp::croak "no specification provided"
58             unless @args;
59 4 50       23 Carp::croak "invalid specification provided"
60             unless ref($args[0]) eq 'HASH';
61 27     27   157 no strict 'refs';
  27         51  
  27         1019  
62 27     27   126 no warnings 'once';
  27         47  
  27         7503  
63 4         10 ${$target . '::app_easer_spec'} = shift @args;
  4         50  
64             } ## end elsif ($request eq '-spec')
65             elsif ($request eq '-parent') { # 2024-08-28 EXPERIMENTAL
66 0 0       0 Carp::croak "no parent class provided"
67             unless @args;
68 0         0 $parent_class = shift @args;
69              
70             # make sure it's required
71 0         0 App::Easer::V2::Command->load_module($parent_class);
72             }
73 4         15 else { push @args_for_exporter, $request }
74             } ## end while (@args)
75 8         440153 $package->export_to_level(1, $package, @args_for_exporter);
76             } ## end sub import
77              
78             package App::Easer::V2::Command;
79 27     27   192 use Scalar::Util 'blessed';
  27         48  
  27         1675  
80 27     27   178 use List::Util 'any';
  27         47  
  27         1890  
81 27     27   16043 use English '-no_match_vars';
  27         39664  
  27         180  
82 27     27   11050 use Scalar::Util qw< weaken >;
  27         56  
  27         108435  
83              
84             # some stuff can be managed via a hash reference kept in a "slot",
85             # allowing for overriding should be easy either with re-defining the
86             # "slot" method, or overriding the sub-method relying on it. The name of
87             # the slot is the same as the name of the actual package that $self is
88             # blessed into.
89 13984   50 13984   17565 sub slot ($self) { return $self->{blessed($self)} //= {} }
  13984         17894  
  13984         17601  
  13984         46137  
90              
91             # This is a poor man's way to easily define attributes in a single line
92             # Corinna will be a blessing eventually
93 12313     12313   55006 sub _rwn ($self, $name, @newval) {
  12313         16584  
  12313         17892  
  12313         16163  
  12313         14895  
94 12313         21920 my $vref = \$self->slot->{$name};
95 12313 100       25672 $$vref = $newval[0] if @newval;
96 12313         44598 return $$vref;
97             }
98              
99 5465     5465   7553 sub _rw ($s, @n) { $s->_rwn((caller(1))[3] =~ s{.*::}{}rmxs, @n) }
  5465         7416  
  5465         7231  
  5465         6981  
  5465         45769  
100              
101 4173     4173   5575 sub _rwa ($self, @n) {
  4173         5397  
  4173         5316  
  4173         5301  
102 4173         41646 my $aref = $self->_rwn((caller(1))[3] =~ s{.*::}{}rmxs, @n);
103 4173 50       15825 Carp::confess() unless defined $aref;
104 4173         12225 return $aref->@*;
105             }
106              
107 182     182   268 sub _rwad ($self, @n) {
  182         275  
  182         266  
  182         269  
108 182   50     1738 my $aref = $self->_rwn((caller(1))[3] =~ s{.*::}{}rmxs, @n) // [];
109 182 50       1124 return wantarray ? $aref->@* : [$aref->@*];
110             }
111              
112 326     326   458 sub _rw_prd ($self, @n) {
  326         465  
  326         505  
  326         397  
113 326         568 my $slot = $self->slot;
114 326         3182 my $name = (caller(1))[3] =~ s{.*::}{}rmxs;
115 326 50       2577 if (@n) {
    100          
116 0         0 $slot->{$name} = $n[0];
117             }
118             elsif (ref(my $ref_to_default = $slot->{$name})) {
119 166         512 my $parent = $self->parent;
120 166 100       659 $slot->{$name} = $parent ? $parent->$name : $$ref_to_default;
121             }
122 326         1689 return $slot->{$name};
123             }
124              
125             # these "attributes" would point to stuff that is normally "scalar" and
126             # used as specification overall. It can be overridden but probably it's
127             # just easier to stick in a hash inside the slot. We don't want to put
128             # executables here, though - overriding should be the guiding principle
129             # in this case.
130 182     182   266 sub aliases ($self, @r) {
  182         252  
  182         330  
  182         246  
131 182 100       413 if (my @aliases = $self->_rwad(@r)) { return @aliases }
  111         747  
132 71 50       160 if (defined(my $name = $self->_rwn('name'))) { return $name }
  71         438  
133 0         0 return;
134             }
135 354     354   577 sub allow_residual_options ($self, @r) { $self->_rw(@r) }
  354         540  
  354         627  
  354         463  
  354         840  
136 235     235   307 sub auto_environment ($self, @r) { $self->_rw(@r) }
  235         424  
  235         286  
  235         287  
  235         430  
137 257     257   383 sub call_name ($self, @r) { $self->_rw(@r) }
  257         407  
  257         456  
  257         352  
  257         691  
138 602     602   911 sub children ($self, @r) { $self->_rwa(@r) }
  602         853  
  602         832  
  602         797  
  602         1272  
139 1204     1204   1845 sub children_prefixes ($self, @r) { $self->_rwa(@r) }
  1204         1795  
  1204         1759  
  1204         1609  
  1204         2788  
140 80     80   113 sub default_child ($self, @r) { $self->_rw(@r) }
  80         139  
  80         154  
  80         109  
  80         171  
141 31     31   98 sub description ($self, @r) { $self->_rw(@r) }
  31         56  
  31         78  
  31         44  
  31         77  
142 111     111   149 sub environment_prefix ($self, @r) { $self->_rw(@r) }
  111         145  
  111         167  
  111         169  
  111         221  
143 123     123   244 sub execution_reason ($self, @r) { $self->_rw(@r) }
  123         191  
  123         239  
  123         172  
  123         331  
144 5     5   12 sub fallback_to ($self, @r) { $self->_rw(@r) }
  5         17  
  5         10  
  5         8  
  5         16  
145 102     102   233 sub final_commit_stack ($self, @r) { $self->_rwa(@r) }
  102         230  
  102         185  
  102         150  
  102         237  
146 602     602   843 sub force_auto_children ($self, @r) { $self->_rw(@r) }
  602         821  
  602         821  
  602         862  
  602         1254  
147 248     248   358 sub hashy_class ($self, @r) { $self->_rw(@r) }
  248         390  
  248         442  
  248         362  
  248         534  
148 43     43   76 sub help ($self, @r) { $self->_rw(@r) }
  43         65  
  43         61  
  43         71  
  43         136  
149 40     40   77 sub help_channel ($slf, @r) { $slf->_rw(@r) }
  40         74  
  40         61  
  40         76  
  40         101  
150 91   66 91   1051 sub name ($s, @r) { $s->_rw(@r) // ($s->aliases)[0] // '**no name**' }
  91   50     137  
  91         140  
  91         144  
  91         203  
151 34     34   60 sub options_help ($s, @r) { $s->_rw(@r) }
  34         76  
  34         54  
  34         59  
  34         83  
152 217     217   309 sub params_validate ($self, @r) { $self->_rw(@r) }
  217         346  
  217         334  
  217         417  
  217         473  
153 1310     1310   2038 sub parent ($self, @r) { $self->_rw(@r) }
  1310         1765  
  1310         1787  
  1310         1657  
  1310         2619  
154 951     951   1278 sub pre_execute ($self, @r) { $self->_rwa(@r) }
  951         2991  
  951         1297  
  951         1151  
  951         2008  
155 537     537   1194 sub residual_args ($self, @r) { $self->_rwa(@r) }
  537         794  
  537         802  
  537         676  
  537         1150  
156 232     232   476 sub _last_cmdline ($self, @r) { $self->_rw(@r) }
  232         473  
  232         486  
  232         378  
  232         503  
157 289     289   471 sub _sources ($self, @r) { $self->_rwn(sources => @r) }
  289         427  
  289         422  
  289         366  
  289         620  
158 34     34   57 sub usage ($self, @r) { $self->_rw(@r) }
  34         59  
  34         52  
  34         48  
  34         114  
159              
160 326     326   495 sub config_hash_key ($self, @r) { $self->_rw_prd(@r) }
  326         457  
  326         504  
  326         461  
  326         868  
161              
162 0     0   0 sub is_root ($self) { ! defined($self->parent) }
  0         0  
  0         0  
  0         0  
163 0     0   0 sub root ($self) {
  0         0  
  0         0  
164 0         0 my $slot = $self->slot;
165 0   0     0 return $slot->{root} //= do {
166 0         0 my $retval = $self;
167 0         0 while (defined(my $parent = $retval->parent)) {
168 0         0 $retval = $parent;
169             }
170 0         0 $retval;
171             };
172             }
173              
174 276     276   412 sub child ($self, @newval) {
  276         409  
  276         455  
  276         402  
175 276         609 my $slot = $self->slot;
176 276 50       645 if (@newval) {
177 276         643 $slot->{child} = $newval[0];
178 276         539 weaken($slot->{child});
179             }
180 276         541 return $slot->{child};
181             }
182 0     0   0 sub is_leaf ($self) { ! defined($self->child) }
  0         0  
  0         0  
  0         0  
183 0     0   0 sub leaf ($self) {
  0         0  
  0         0  
184 0         0 my $slot = $self->slot;
185 0 0       0 if (! exists($slot->{leaf})) {
186 0         0 my $retval = $self;
187 0         0 while (defined(my $parent = $retval->child)) {
188 0         0 $retval = $parent;
189             }
190 0         0 $slot->{leaf} = $retval;
191 0         0 weaken($slot->{leaf});
192             }
193 0         0 return $slot->{leaf};
194             }
195              
196              
197             # 2024-08-27 expand to allow hashref in addition to arrayref
198             # backwards-compatibility contract is that overriding this function allows
199             # returning the list of sources to use, which might be composed of a single
200             # hashref...
201 615     615   1002 sub sources ($self, @new) {
  615         991  
  615         973  
  615         816  
202 615         928 my $r;
203 615         1118 my $slot = $self->slot;
204 615 50       1259 if (@new) { # setter + getter
205 0         0 $r = $slot->{sources} = $new[0];
206             }
207             else { # getter only, set default if *nothing* has been set yet
208 615         1035 state $default_array =
209             [ qw< +CmdLine +Environment +Parent=70 +Default=100 > ];
210 615         920 state $default_hash = {
211             current => [ qw< +CmdLine +Environment +Default +ParentSlices > ],
212             final => [ ],
213             };
214 615         935 state $default_hash_v2_008 = {
215             current => [ qw< +CmdLine +Environment +Default +ParentSlices > ],
216             final => [ ],
217             };
218 615         1098 $r = $slot->{sources};
219             $r = $slot->{sources} =
220 615 0       1981 ! defined($r) ? Carp::confess()
    0          
    50          
    50          
    100          
221             : $r eq 'default-array' ? $default_array
222             : $r eq 'default-hash' ? $default_hash
223             : $r eq 'v2.008' ? $default_hash_v2_008
224             : Carp::confess()
225             unless ref($r); # string-based, get either default
226             }
227 615 50       1381 Carp::confess() unless defined($r);
228              
229 615 100       2901 return $r->@* if ref($r) eq 'ARRAY'; # backwards-compatible behaviour
230 60 50       211 return \$r if ref($r) eq 'HASH'; # new behaviour
231 0         0 Carp::confess(); # unsupported condition
232             }
233              
234             # getter only
235 340     340   521 sub _sources_for_phase ($self, $phase) {
  340         504  
  340         568  
  340         465  
236 340         940 my @sources = $self->sources; # might call an overridden thing
237              
238 47         236 return ${$sources[0]}->{$phase}
239             if @sources == 1
240             && ref($sources[0]) eq 'REF'
241 340 50 66     1213 && ref(${$sources[0]}) eq 'HASH';
  47   66     188  
242              
243             # backwards compatibility means that we only support the "current"
244             # phase and do nothing for other ones.
245 293 100       1250 return $phase eq 'current' ? \@sources : ();
246             }
247              
248 199     199   316 sub supports ($self, $what) {
  199         277  
  199         406  
  199         368  
249 199     228   1253 any { $_ eq $what } $self->aliases;
  228         2251  
250             }
251              
252 777     777   1125 sub options ($self, @r) {
  777         1201  
  777         1055  
  777         1025  
253 777         1977 return map { $self->resolve_options($_) } $self->_rwa(@r);
  1833         3562  
254             }
255              
256 1833     1833   2282 sub resolve_options ($self, $spec) {
  1833         2180  
  1833         2231  
  1833         2079  
257 1833 100       5176 return $spec if ref($spec) eq 'HASH';
258 104 50       344 $spec = [inherit_options => $spec] unless ref $spec;
259 104 50       248 Carp::confess("invalid spec $spec") unless ref($spec) eq 'ARRAY';
260 104         242 my ($method_name, @names) = $spec->@*;
261 104 50       417 my $method = $self->can($method_name)
262             or Carp::confess("cannot find method $method_name in $self");
263 104         227 return $self->$method(@names);
264             } ## end sub resolve_options
265              
266 104     104   132 sub inherit_options ($self, @names) {
  104         164  
  104         178  
  104         140  
267 104         152 my %got;
268             map {
269 104         175 my @options;
  104         141  
270 104 100       303 if ($_ eq '+parent') {
271 57   100     147 @options = grep { $_->{transmit} // 0 } $self->parent->options;
  287         775  
272             }
273             else {
274 47 50       123 my $name_exact = ref($_) ? undef : $_;
275 47         666 my $name_rx = qr{\A(?:$_)\z};
276 47         102 my $ancestor = $self->parent;
277 47         85 while ($ancestor) {
278             push @options, my @pass = # FIXME something's strange here
279             grep {
280 47         84 my $name = $self->name_for_option($_);
  309         474  
281             ($_->{transmit} // 0)
282             && (! $got{$name}++) # inherit once only
283             && (
284             (defined($name_exact) && $name eq $name_exact)
285 309 100 66     1920 || (! $_->{transmit_exact} && $name =~ m{$name_rx})
      100        
      100        
      50        
      66        
286             );
287             } $ancestor->options;
288 47         97 $ancestor = $ancestor->parent;
289             } ## end while ($ancestor)
290             } ## end else [ if ($_ eq '+parent') ]
291 104         272 map { +{transmit => 1, $_->%*, inherited => 1} } @options;
  308         2053  
292             } @names;
293             } ## end sub inherit_options
294              
295 396     396   715 sub new ($pkg, @args) {
  396         673  
  396         614  
  396         664  
296 27     27   261 my $pkg_spec = do { no strict 'refs'; ${$pkg . '::app_easer_spec'} };
  27         50  
  27         422833  
  396         652  
  396         680  
  396         1827  
297 396 100 100     8833 my $slot = {
      66        
298             aliases => [],
299             allow_residual_options => 0,
300             auto_environment => 0,
301             children => [],
302             children_prefixes => [$pkg . '::Cmd'],
303             config_hash_key => \'merged',
304             default_child => 'help',
305             environment_prefix => '',
306             fallback_to => undef,
307             final_commit_stack => [],
308             force_auto_children => undef,
309             hashy_class => __PACKAGE__,
310             help_channel => '-STDOUT:encoding(UTF-8)',
311             options => [],
312             params_validate => undef,
313             pre_execute => [],
314             residual_args => [],
315             sources => 'default-array', # 2024-08-24 defer
316             ($pkg_spec // {})->%*,
317             (@args && ref $args[0] ? $args[0]->%* : @args),
318             };
319 396         1779 my $self = bless {$pkg => $slot}, $pkg;
320 396         1238 return $self;
321             } ## end sub new
322              
323 1822     1822   2434 sub merge_hashes ($self, @hrefs) { # FIXME this seems way more complicated than needed
  1822         2631  
  1822         2867  
  1822         2508  
324 1822         2559 my (%retval, %is_overridable);
325 1822         3173 for my $href (@hrefs) {
326 4938         9026 for my $src_key (keys $href->%*) {
327 3082         4278 my $dst_key = $src_key;
328 3082         4714 my $this_overridable = 0;
329             $retval{$dst_key} = $href->{$src_key}
330 3082 100 66     11677 if $is_overridable{$dst_key} || !exists($retval{$dst_key});
331 3082 50       7844 $is_overridable{$dst_key} = 0 unless $this_overridable;
332             } ## end for my $src_key (keys $href...)
333             } ## end for my $href (@hrefs)
334 1822         4258 return \%retval;
335             } ## end sub merge_hashes
336              
337 234     234   351 sub _collect ($self, $sources, @args) {
  234         405  
  234         380  
  234         466  
  234         367  
338 234         359 my @residual_args; # what is left from the @args at the end
339              
340 234         513 my $slot = $self->slot;
341 234         477 my $last_priority = 0;
342 234         625 for my $source ($sources->@*) {
343 911 100       2684 my ($src, @opts) = ref($source) eq 'ARRAY' ? $source->@* : $source;
344 911 100 100     2549 my $meta = (@opts && ref $opts[0]) ? shift @opts : {};
345 911         1450 my $locator = $src;
346 911 100       2051 if (! ref($src)) {
347 909         2856 ($src, my $priority) = split m{=}mxs, $src;
348 909 100       2579 $meta->{priority} = $priority if defined $priority;
349 909         3884 $locator = $src =~ s{\A \+}{source_}rmxs;
350             }
351 911 50       2638 my $sub = $self->ref_to_sub($locator)
352             or die "unhandled source for $locator\n";
353              
354 911         2623 my ($slice, $residuals) = $sub->($self, \@opts, \@args);
355 911 100       18847 push @residual_args, $residuals->@* if defined $residuals;
356              
357             # whatever happened in the source, it might have changed the
358             # internals and we need to re-load them from the current config
359 911   100     1786 my $latest = $self->_rwn('config') // {};
360 911   100     3312 my @sequence = ($latest->{sequence} //= [])->@*; # legacy
361 911   100     3980 my %all_eslices_at = ($latest->{all_eslices_at} // {})->%*; # v2.8
362 911   100     3408 my %command_eslices_at = ($latest->{command_eslices_at} // {})->%*;
363              
364             # only operate if the source returned something to track
365 911 100       2142 if ($slice) {
366             $last_priority = my $priority
367 908   66     3206 = $meta->{priority} //= $last_priority + 10;
368              
369 908         2389 my $eslice = [$priority, $src, \@opts, $locator, $slice];
370              
371             # new way of collecting the aggregated configuration
372             # the merge takes into account priorities across all command
373             # layers, this function encapsulates getting all of them
374 908   100     4143 push(($all_eslices_at{$priority} //= [])->@*, $eslice);
375 908   100     3334 push(($command_eslices_at{$priority} //= [])->@*, $eslice);
376              
377             # older way of collecting the aggregated configuration
378 908         1536 push @sequence, $eslice;
379 908         2467 for (my $i = $#sequence; $i > 0; --$i) {
380 763 100       3065 last if $sequence[$i - 1][0] <= $sequence[$i][0];
381 73         286 @sequence[$i - 1, $i] = @sequence[$i, $i - 1];
382             }
383             }
384              
385             # whatever happened, re-compute the aggregated configuration in the
386             # new "matrix" way and in the legacy way
387             my $matrix_config = $self->merge_hashes(
388 2472         4958 map { $_->[-1] } # take slice out of eslice
389 2331         4544 map { $all_eslices_at{$_}->@* } # unroll all eslices
390 911         3797 sort { $a <=> $b } # sort by priority
  2048         4661  
391             keys(%all_eslices_at) # keys is the priority
392             );
393 911         1993 my $legacy_config = $self->merge_hashes(map {$_->[-1]} @sequence);
  2466         4428  
394              
395             # save configuration at each step, so that each following source
396             # can take advantage of configurations collected so far. This is
397             # important for e.g. sources that load options from files whose
398             # path is provided as an option itself.
399 911         16635 $self->_rwn(
400             config => {
401             merged => $legacy_config,
402             merged_legacy => $legacy_config,
403             'v2.008' => $matrix_config,
404             sequence => \@sequence,
405             all_eslices_at => \%all_eslices_at,
406             command_eslices_at => \%command_eslices_at,
407             }
408             );
409             } ## end for my $source ($self->...)
410             #App::Easer::V2::d(config => $self->_rwn('config'));
411              
412             # return what's left
413 234         1129 return \@residual_args;
414             }
415              
416 217     217   339 sub collect ($self, @args) {
  217         367  
  217         396  
  217         295  
417 217 50       713 if (my $sources = $self->_sources_for_phase('current')) {
418 217         768 $self->residual_args($self->_collect($sources, @args));
419             }
420 217         628 return $self;
421             } ## end sub collect
422              
423             # last round of configuration options collection
424 123     123   194 sub final_collect ($self) {
  123         220  
  123         166  
425 123 100       435 if (my $sources = $self->_sources_for_phase('final')) {
426 17         48 $self->_collect($sources);
427             }
428 123         241 return $self;
429             } ## end sub collect
430              
431 217     217   357 sub getopt_config ($self, @n) {
  217         342  
  217         327  
  217         301  
432 217         570 my $value = $self->_rw(@n);
433 217 50       674 if (!defined $value) {
434 217         671 my @r = qw< gnu_getopt >;
435 217 100       828 push @r, qw< require_order pass_through > if $self->list_children;
436 217 50       809 push @r, qw< pass_through > if $self->allow_residual_options;
437 217         555 $value = $self->_rw(\@r);
438             } ## end if (!defined $value)
439 217         1267 return $value->@*;
440             } ## end sub getopt_config
441              
442             # This source is not supposed to accept "options", although it might in
443             # the future, e.g. to set a specific getopt_config instead of setting it
444             # as a general parameter. On the other hand, it does focus on processing
445             # $args
446 217     217   322 sub source_CmdLine ($self, $ignore, $args) {
  217         367  
  217         355  
  217         352  
  217         341  
447 217         616 my @args = $args->@*;
448              
449 217         19748 require Getopt::Long;
450 217         294416 Getopt::Long::Configure('default', $self->getopt_config);
451              
452 217         16785 my (%option_for, @specs, %name_for);
453 217         840 for my $option ($self->options) {
454 488 100       1158 next unless exists($option->{getopt});
455 483         910 my $go = $option->{getopt};
456 483 50       1000 if (ref($go) eq 'ARRAY') {
457 0         0 my ($string, $callback) = $go->@*;
458 0     0   0 push @specs, $string, sub { $callback->(\%option_for, @_) };
  0         0  
459 0         0 $go = $string;
460             }
461             else {
462 483         860 push @specs, $go;
463             }
464              
465 483         1710 my ($go_name) = $go =~ m{\A(\w[-\w]*)}mxs;
466 483         1051 my $official_name = $self->name_for_option($option);
467 483 100       1295 $name_for{$go_name} = $official_name if $go_name ne $official_name;
468             }
469              
470 217 50       951 Getopt::Long::GetOptionsFromArray(\@args, \%option_for, @specs)
471             or die "bailing out\n";
472              
473             # Check if we want to forbid the residual @args to start with a '-'
474 217         94360 my $strict = !$self->allow_residual_options;
475 217 50 66     1450 die "bailing out (allow_residual_options is false and got <@args>)"
      66        
476             if $strict && @args && $args[0] =~ m{\A - . }mxs;
477              
478             # remap names where the official one is different from the getopt one
479 217         1043 $self->_rename_options_inplace(\%option_for, \%name_for);
480              
481 217         1352 $self->_last_cmdline( { option_for => \%option_for, args => \@args });
482              
483 217         1485 return (\%option_for, \@args);
484             } ## end sub source_CmdLine
485              
486 217     217   343 sub _rename_options_inplace ($self, $collected, $name_for) {
  217         348  
  217         357  
  217         334  
  217         314  
487 217         373 my %renamed;
488 217         725 for my $go_name (sort { $a cmp $b } keys $name_for->%*) {
  5         17  
489 10 100       22 next unless exists $collected->{$go_name};
490 4         6 my $official_name = $name_for->{$go_name};
491 4         15 $renamed{$official_name} = delete($collected->{$go_name});
492             }
493 217         483 $collected->{$_} = $renamed{$_} for keys %renamed;
494 217         488 return $self;
495             }
496              
497 15     15   25 sub source_LastCmdLine ($self, @ignore) {
  15         44  
  15         28  
  15         21  
498 15 50       64 my $last = $self->_last_cmdline or return {};
499 15         46 return $last->{option_for};
500             }
501              
502 1305     1305   1708 sub name_for_option ($self, $o) {
  1305         1586  
  1305         1741  
  1305         1425  
503 1305 100       2627 return $o->{name} if defined $o->{name};
504             return $1
505 1275 50 33     8471 if defined $o->{getopt} && $o->{getopt} =~ m{\A(\w[-\w]*)}mxs;
506             return lc $o->{environment}
507 0 0 0     0 if defined $o->{environment} && $o->{environment} ne '1';
508 0         0 return '~~~';
509             } ## end sub name_for_option
510              
511 205     205   349 sub source_Default ($self, $opts, @ignore) {
  205         314  
  205         327  
  205         342  
  205         277  
512 205         417 my %opts = $opts->@*;
513 205         397 my $include_inherited = $opts{include_inherited};
514             return {
515 235         485 map { $self->name_for_option($_) => $_->{default} }
516 393         820 grep { exists $_->{default} }
517 205 100       541 grep { $include_inherited || !$_->{inherited} } $self->options
  428         1466  
518             };
519             }
520 15     15   26 sub source_FinalDefault ($self, @i) {
  15         22  
  15         28  
  15         18  
521 15         79 return $self->source_Default([ include_inherited => 1]);
522             }
523              
524 3     3   6 sub source_FromTrail ($self, $trail, @ignore) {
  3         5  
  3         5  
  3         5  
  3         5  
525 3         7 my $conf = $self->config_hash;
526 3         9 for my $key ($trail->@*) {
527 9 50       20 return {} unless defined $conf->{$key};
528 9         12 $conf = $conf->{$key};
529 9 50       20 die "invalid trail $trail->@* for configuration gathering"
530             unless ref($conf) eq 'HASH';
531             } ## end for my $key ($keys->@*)
532 3         8 return $conf;
533             }
534              
535 463     463   649 sub environment_variable_name ($self, $ospec) {
  463         622  
  463         593  
  463         632  
536             my $env =
537             exists $ospec->{environment} ? $ospec->{environment}
538 463 100       1247 : $self->auto_environment ? 1
    100          
539             : undef;
540 463 100 100     1875 return $env unless ($env // '') eq '1';
541              
542             # get prefixes all the way up to the first command
543 102         140 my @prefixes;
544 102         232 for (my $instance = $self; $instance; $instance = $instance->parent) {
545 111   50     259 unshift @prefixes, $instance->environment_prefix // '';
546             }
547              
548 102         267 my $name = $self->name_for_option($ospec) =~ s{\W+}{_}rgmxs;
549 102         413 return uc(join '', @prefixes, $name);
550             } ## end sub environment_variable_name
551              
552              
553 209     209   345 sub source_Environment ($self, $opts, @ignore) {
  209         329  
  209         295  
  209         323  
  209         292  
554 209         561 my %opts = $opts->@*;
555 209         433 my $include_inherited = $opts{include_inherited};
556             return {
557             map {
558 414         985 my $en = $self->environment_variable_name($_);
559             defined($en)
560             && exists($ENV{$en})
561 414 100 100     1848 ? ($self->name_for_option($_) => $ENV{$en})
562             : ();
563 209 100       538 } grep { $include_inherited || !$_->{inherited} } $self->options
  454         1679  
564             };
565             } ## end sub source_Environment
566 15     15   25 sub source_FinalEnvironment ($self, @i) {
  15         22  
  15         32  
  15         19  
567 15         79 return $self->source_Environment([ include_inherited => 1 ]);
568             }
569              
570 33     33   51 sub source_JsonFileFromConfig ($self, $key, @ignore) {
  33         50  
  33         70  
  33         70  
  33         40  
571 33   50     133 $key = $key->[0] // 'config';
572 33 100       104 defined(my $filename = $self->config($key)) or return {};
573 10         81 require JSON::PP;
574 10         36 return JSON::PP::decode_json($self->slurp($filename));
575             } ## end sub source_JsonFileFromConfig
576              
577 10     10   21 sub slurp ($self, $file, $mode = '<:encoding(UTF-8)') {
  10         20  
  10         19  
  10         21  
  10         19  
578 10 50       601 open my $fh, $mode, $file or die "open('$file'): $!\n";
579 10         5139 local $/;
580 10         346 return <$fh>;
581             }
582              
583 0     0   0 sub source_JsonFiles ($self, $candidates, @ignore) {
  0         0  
  0         0  
  0         0  
  0         0  
584 0         0 require JSON::PP;
585             return $self->merge_hashes(
586 0         0 map { JSON::PP::decode_json($self->slurp($_)) }
587 0         0 grep { -e $_ } $candidates->@*
  0         0  
588             );
589             } ## end sub source_JsonFiles
590              
591 223     223   376 sub source_Parent ($self, @ignore) {
  223         369  
  223         401  
  223         283  
592 223 100       628 my $parent = $self->parent or return {};
593 104         374 return $parent->config_hash(0);
594             }
595              
596 3     3   4 sub source_ParentSlices ($self, @ignore) {
  3         3  
  3         4  
  3         4  
597 3 100       6 my $parent = $self->parent or return; # no Parent, no Party
598              
599 1         3 my $latest = $self->_rwn('config');
600 1 50       3 $self->_rwn(config => ($latest = {})) unless defined $latest;
601 1   50     3 my $all_eslices_at = $latest->{all_eslices_at} //= {};
602              
603             # get all stuff from parent, keeping priorities.
604 1   50     3 my $pslices_at = $parent->config_hash(1)->{all_eslices_at} // {};
605 1         3 for my $priority (keys($pslices_at->%*)) {
606 3   50     5 my $eslices = $all_eslices_at->{$priority} //= [];
607 3         5 push $eslices->@*, $pslices_at->{$priority}->@*;
608             }
609              
610 1         3 return;
611             }
612              
613             # get the assembled config for the command. It supports the optional
614             # additional boolean parameter $blame to get back a more structured
615             # version where it's clear where each option comes from, to allow for
616             # further injection of parameters from elsewhere.
617 273     273   922 sub config_hash ($self, $blame = 0) {
  273         434  
  273         434  
  273         632  
618 273   50     622 my $config = $self->_rwn('config') // {};
619 273 100       665 return $config if $blame;
620 272   50     704 return $config->{$self->config_hash_key} // {};
621             }
622              
623             # get one or more specific configurtion values
624 59     59   207 sub config ($self, @keys) {
  59         91  
  59         101  
  59         78  
625 59         136 my $hash = $self->config_hash(0);
626 59 50       300 return $hash->{$keys[0]} if @keys == 1;
627 0         0 return $hash->@{@keys};
628             }
629              
630 0     0   0 sub set_config ($self, $key, @value) {
  0         0  
  0         0  
  0         0  
  0         0  
631 0         0 my $hash = $self->config_hash(0);
632 0         0 delete $hash->{$key};
633 0 0       0 $hash->{$key} = $value[0] if @value;
634 0         0 return $self;
635             } ## end sub set_config
636              
637             # totally replace whatever has been collected at this level
638 0     0   0 sub set_config_hash ($self, $new, $full = 0) {
  0         0  
  0         0  
  0         0  
  0         0  
639 0 0       0 if (! $full) {
640 0   0     0 my $previous = $self->_rwn('config') // {};
641 0         0 my $key = $self->config_hash_key;
642 0         0 $new = { $previous->%*, merged => $new, override => $new };
643             }
644 0         0 $self->_rwn(config => $new);
645 0         0 return $self;
646             }
647              
648 0     0   0 sub inject_configs ($self, $data, $priority = 1000) {
  0         0  
  0         0  
  0         0  
  0         0  
649              
650             # we define an on-the-fly source and get it considered through the
651             # regular source-handling mechanism by _collect
652 0         0 $self->_collect(
653             [
654 0     0   0 sub ($self, $opts, $args) {
  0         0  
  0         0  
  0         0  
655 0         0 my $latest = $self->_rwn('config');
656 0 0       0 $self->_rwn(config => ($latest = {})) unless $latest;
657 0   0     0 my $queue = $latest->{all_eslices_at}{$priority} //= [];
658 0         0 push $queue->@*, [ $priority, injection => [], '', $data ];
659 0         0 return;
660             },
661 0         0 ]
662             );
663             }
664              
665             # (intermediate) commit collected options values, called after collect ends
666 217     217   379 sub commit ($self, @n) {
  217         351  
  217         336  
  217         316  
667 217         542 my $commit = $self->_rw(@n);
668 217 50       543 return $commit if @n; # setter, don't call the commit callback
669 217 100       589 return unless $commit;
670 19         50 return $self->ref_to_sub($commit)->($self);
671             } ## end sub commit
672              
673             # final commit of collected options values, called after final_collect ends
674             # this method tries to "propagate" the call up to the parent (and the root
675             # eventually) unless told not to do so. This should allow concentrating
676             # some housekeeping operations in the root command while still waiting for
677             # all options to have been collected
678 220     220   374 sub final_commit ($self, @n) {
  220         325  
  220         335  
  220         287  
679 220 50       502 return $self->_rw(@n) if @n; # setter, don't call the callback
680              
681             # we operate down at the slot level because we want to separate the case
682             # where key 'final_commit' is absent (defaulting to propagation up to
683             # the parent) and where it's set but otherwise false (in which case
684             # there is no propagation).
685 220         508 my $slot = $self->slot;
686              
687             # put "myself" onto the call stack for final_commit
688 220   50     589 my $stack = $slot->{final_commit_stack} //= [];
689 220         541 push $stack->@*, $self;
690              
691 220 100       531 if (exists($slot->{final_commit})) {
692 5         10 my $commit = $slot->{final_commit};
693              
694             # if $commit is false (but present, because it exists) then we
695             # stop and do not propagate to the parent
696 5 50       12 return unless $commit;
697              
698             # otherwise, we call it and its return value will tell us whether to
699             # propagate to the parent too or stop here
700 5         16 my $propagate_to_parent = $self->ref_to_sub($commit)->($self);
701 5 50       15 return unless $propagate_to_parent;
702             }
703              
704             # here we try to propagate to the parent... if it exists
705 220         533 my $parent = $self->parent;
706 220 100       637 return unless $parent; # we're root, no parent, no propagation up
707              
708 97         392 $parent->final_commit_stack([$stack->@*]);
709 97         376 return $parent->final_commit;
710             } ## end sub commit
711              
712             # validate collected options values, called after commit ends.
713 217     217   357 sub validate ($self, @n) {
  217         350  
  217         306  
  217         365  
714              
715             # Support the "accessor" interface for using a validation sub
716 217         486 my $validator = $self->_rw(@n);
717 217 50       572 return $validator if @n;
718              
719             # If set, it MUST be a validation sub reference. Otherwise, try the
720             # params_validate/Params::Validate path.
721 217 50       940 if ($validator) {
    50          
722 0 0       0 die "validator can only be a CODE reference\n"
723             unless ref $validator eq 'CODE';
724 0         0 $validator->($self);
725             }
726             elsif (my $params_validate = $self->params_validate) {
727 0         0 require Params::Validate;
728 0 0 0     0 if (my $config_validator = $params_validate->{config} // undef) {
729 0         0 my @array = $self->config_hash;
730 0         0 &Params::Validate::validate(\@array, $config_validator);
731             }
732 0 0 0     0 if (my $args_validator = $params_validate->{args} // undef) {
733 0         0 my @array = $self->residual_args;
734 0         0 &Params::Validate::validate_pos(\@array, $args_validator->@*);
735             }
736             }
737             else {} # no validation needed
738              
739 217         418 return $self;
740             } ## end sub validate ($self)
741              
742 127     127   217 sub find_matching_child ($self, $command) {
  127         211  
  127         220  
  127         196  
743 127 50       359 return unless defined $command;
744 127         410 for my $candidate ($self->list_children) {
745 199         854 my ($child) = $self->inflate_children($candidate);
746 199 100       714 return $child if $child->supports($command);
747             }
748 5         48 return;
749             } ## end sub find_matching_child
750              
751 40     40   69 sub _inflate_default_child ($self) {
  40         63  
  40         55  
752 40 50       105 defined(my $default = $self->default_child)
753             or die "undefined default child\n";
754 40 100       263 return undef if $default eq '-self';
755 4 50       13 my $child = $self->find_matching_child($default)
756             or die "no child matching the default $default\n";
757 4         20 return $child;
758             } ## end sub inflate_default_child ($self)
759              
760             # look for a child to hand execution over. Returns an child instance or
761             # undef (which means that the $self is in charge of executing
762             # something). This implements the most sensible default, deviations will
763             # have to be coded explicitly.
764             # Return values:
765             # - (undef, '-leaf') if no child exists
766             # - ($instance, @args) if a child is found with $args[0]
767             # - ($instance, '-default') if the default child is returned
768             # - (undef, '-fallback') in case $self is the fallback
769             # - ($instance, '-fallback', @args) in case the fallback is returned
770 217     217   317 sub find_child ($self) {
  217         371  
  217         297  
771 217 100       554 my @candidates = $self->list_children or return (undef, '-leaf');
772 131         383 my @residuals = $self->residual_args;
773 131 100       397 if (@residuals) {
    50          
774 91 100       402 if (my $child = $self->find_matching_child($residuals[0])) {
775 86         513 return ($child, @residuals);
776             } # otherwise... see what the fallback is about
777             }
778             elsif (defined(my $default = $self->default_child)) {
779 40         142 return ($self->_inflate_default_child, '-default');
780             }
781              
782             # try the fallback...
783 5         22 my $fallback = $self->fallback;
784 5 100       19 if (defined $fallback) {
785 1 50       4 return (undef, '-fallback') if $fallback eq '-self';
786 0 0       0 return ($self->_inflate_default_child, '-default')
787             if $fallback eq '-default';
788 0 0       0 if (my $child = $self->find_matching_child($fallback)) {
789 0         0 return ($child, -fallback => @residuals);
790             }
791             } ## end if (defined $fallback)
792              
793             # no fallback at this point... it's an error, build a message and die!
794             # FIXME this can be improved
795 4         180 die "cannot find sub-command '$residuals[0]'\n";
796             } ## end sub find_child ($self)
797              
798             # get the list of children. This only gives back a list of "hints" that
799             # can be turned into instances via inflate_children. In this case, it's
800             # module names
801 602     602   931 sub list_children ($self) {
  602         875  
  602         837  
802 602         1579 my @children = $self->children;
803              
804             # handle auto-loading of children from modules in @INC via prefixes
805 602         3664 require File::Spec;
806             my @expanded_inc = map {
807 602         1444 my ($v, $dirs) = File::Spec->splitpath($_, 'no-file');
  4303         19635  
808 4303         27238 [$v, File::Spec->splitdir($dirs)];
809             } @INC;
810 602         1142 my %seen;
811             my @autoloaded_children = map {
812 602         1625 my @parts = split m{::}mxs, $_ . 'x';
  602         2516  
813 602         1723 substr(my $bprefix = pop @parts, -1, 1, '');
814             map {
815 602         1140 my ($v, @dirs) = $_->@*;
  4303         14281  
816 4303         29556 my $dirs = File::Spec->catdir(@dirs, @parts);
817 4303 50       79400 if (opendir my $dh, File::Spec->catpath($v, $dirs, '')) {
818 0         0 grep { !$seen{$_}++ }
819             map {
820 0         0 substr(my $lastpart = $_, -3, 3, '');
821 0         0 join '::', @parts, $lastpart;
822             } grep {
823 0         0 my $path = File::Spec->catpath($v, $dirs, $_);
824 0 0 0     0 (-e $path && !-d $path)
      0        
825             && substr($_, 0, length($bprefix)) eq $bprefix
826             && substr($_, -3, 3) eq '.pm'
827 0         0 } sort { $a cmp $b } readdir $dh;
  0         0  
828             } ## end if (opendir my $dh, File::Spec...)
829 4303         17793 else { () }
830             } @expanded_inc;
831             } $self->children_prefixes;
832             push @autoloaded_children, map {
833 602         1934 my $prefix = $_;
  602         986  
834 602         1094 my $prefix_length = length($prefix);
835 44         184 grep { !$seen{$_}++ }
836             grep {
837 602 100       1797 (substr($_, 0, length $prefix) eq $prefix)
  423         1319  
838             && (index($_, ':', $prefix_length) < 0);
839             } keys %App::Easer::V2::registered;
840             } $self->children_prefixes;
841              
842             # auto-loaded children are appended with consistent sorting
843 602         1307 push @children, sort { $a cmp $b } @autoloaded_children;
  0         0  
844              
845 602 100 100     1739 push @children, $self->auto_children
846             if $self->force_auto_children // @children;
847 602         6490 return @children;
848             } ## end sub list_children ($self)
849              
850 1232     1232   1688 sub _auto_child ($self, $name, $inflate = 0) {
  1232         1604  
  1232         1712  
  1232         1743  
  1232         1542  
851 1232         2809 my $child = __PACKAGE__ . '::' . ucfirst(lc($name));
852 1232 100       2294 ($child) = $self->inflate_children($child) if $inflate;
853 1232         3026 return $child;
854             }
855              
856             # returns either class names or inflated objects
857 408     408   675 sub auto_children ($self, $inflate = 0) {
  408         594  
  408         694  
  408         611  
858 408         764 map { $self->_auto_child($_, $inflate) } qw< help commands tree >;
  1224         2395  
859             }
860              
861 1     1   6 sub auto_commands ($self) { return $self->_auto_child('commands', 1) }
  1         2  
  1         2  
  1         4  
862              
863 7     7   25 sub auto_help ($self) { return $self->_auto_child('help', 1) }
  7         16  
  7         10  
  7         21  
864              
865 0     0   0 sub auto_tree ($self) { return $self->_auto_child('tree', 1) }
  0         0  
  0         0  
  0         0  
866              
867 5     5   29 sub run_help ($self, $mode = 'help') { $self->auto_help->run($mode) }
  5         7  
  5         10  
  5         6  
  5         14  
868              
869 1     1   66 sub full_help_text ($s, @as) { $s->auto_help->collect_help_for($s, @as) }
  1         1  
  1         2  
  1         1  
  1         3  
870              
871 0     0   0 sub load_module ($sop, $module) {
  0         0  
  0         0  
  0         0  
872 0         0 my $file = "$module.pm" =~ s{::}{/}grmxs;
873 0 0       0 eval { require $file } or Carp::confess("module<$module>: $EVAL_ERROR");
  0         0  
874 0         0 return $module;
875             }
876              
877             # Gets a specification like "Foo::Bar::baz" and returns a reference to
878             # sub "baz" in "Foo::Bar". If no package name is set, returns a
879             # reference to a sub in the package of $self. FIXME document properly
880 1006     1006   1488 sub ref_to_sub ($self, $spec) {
  1006         1455  
  1006         1484  
  1006         1296  
881 1006 50       2153 Carp::confess("undefined specification in ref_to_sub")
882             unless defined $spec;
883 1006 100       2413 return $spec if ref($spec) eq 'CODE';
884 909 50       5702 my ($class, $function) =
885             ref($spec) eq 'ARRAY'
886             ? $spec->@*
887             : $spec =~ m{\A (?: (.*) :: )? (.*) \z}mxs;
888 909 100 100     7843 return $self->can($function) unless length($class // '');
889 1 50       31 $self->load_module($class) unless $class->can($function);
890 1         7 return $class->can($function);
891             } ## end sub ref_to_sub
892              
893 276     276   419 sub instantiate ($sop, $class, @args) {
  276         432  
  276         689  
  276         490  
  276         372  
894 276 50       3007 $sop->load_module($class) unless $class->can('new');
895 276         795 return $class->new(@args);
896             }
897              
898 276     276   430 sub _reparent ($self, $child) {
  276         406  
  276         383  
  276         368  
899 276         873 $child->parent($self);
900 276         1423 $self->child($child); # saves a weak reference to $child
901              
902             # 2024-08-27 propagate sources configurations
903 276 100       768 if (! ref($child->_sources)) { # still default, my need to set it
904 275         657 my ($first, @rest) = $self->sources;
905 275 100       876 if (ref($first) eq 'REF') { # new approach, propagate
906 13         25 my $ssources = $$first;
907 13         72 $child->_sources(my $csources = { $ssources->%* });
908 13 50       53 if (my $next = $ssources->{next}) {
909 0 0       0 my @csources =
    0          
910             ref($next) eq 'ARRAY' ? $next->@*
911             : ref($next) eq 'CODE' ? $next->($child)
912             : Carp::confess(); # no clue
913 0         0 $csources->{current} = \@csources;
914             }
915             }
916             }
917              
918             # propagate pre-execute callbacks down the line
919 276         662 $child->pre_execute_schedule($self->pre_execute);
920              
921 276         1120 return $child;
922             }
923              
924             # transform one or more children "hints" into instances.
925 248     248   484 sub inflate_children ($self, @hints) {
  248         398  
  248         492  
  248         328  
926 248         604 my $hashy = $self->hashy_class;
927             map {
928 276         468 my $child = $_;
929 276 50       726 if (!blessed($child)) { # actually inflate it
930 276 100       1318 $child =
    100          
931             ref($child) eq 'ARRAY' ? $self->instantiate($child->@*)
932             : ref($child) eq 'HASH' ? $self->instantiate($hashy, $child)
933             : $self->instantiate($child);
934             } ## end if (!blessed($child))
935 276         731 $self->_reparent($child); # returns $child
936 248         579 } grep { defined $_ } @hints;
  276         666  
937             } ## end sub inflate_children
938              
939             # fallback mechanism when finding a child, relies on fallback_to.
940 5     5   14 sub fallback ($self) {
  5         9  
  5         10  
941 5         18 my $fto = $self->fallback_to;
942 5 50 66     39 return $fto if !defined($fto) || $fto !~ m{\A(?: 0 | [1-9]\d* )\z};
943 0         0 my @children = $self->list_children;
944 0 0       0 return $children[$fto] if $fto <= $#children;
945 0         0 return undef;
946             } ## end sub fallback ($self)
947              
948             # execute what's set as the execute sub in the slot
949 71     71   120 sub execute ($self) {
  71         137  
  71         112  
950 71 50       150 my $spec = $self->_rw or die "nothing to search for execution\n";
951 71 50       227 my $sub = $self->ref_to_sub($spec) or die "nothing to execute\n";
952 71         337 return $sub->($self);
953             }
954              
955 276     276   407 sub pre_execute_schedule ($self, @specs) {
  276         399  
  276         409  
  276         411  
956 276 50       581 if (my $spec = $self->_rw) {
957 0 0       0 my $sub = $self->ref_to_sub($spec) or die "nothing for pre_execute_schedule\n";
958 0         0 return $sub->($self, @specs);
959             }
960              
961             # default approach is to append to the current ones
962 276         814 $self->pre_execute([$self->pre_execute, @specs]);
963 276         523 return $self;
964             }
965              
966 123     123   223 sub pre_execute_run ($self) {
  123         197  
  123         173  
967 123 50       356 if (my $spec = $self->_rw) {
968 0 0       0 my $sub = $self->ref_to_sub($spec) or die "nothing to pre-execute\n";
969 0         0 return $sub->($self);
970             }
971              
972             # default is to run 'em all
973 123         347 for my $spec ($self->pre_execute) {
974 0 0       0 my $sub = $self->ref_to_sub($spec) or die "nothing to pre-execute\n";
975 0         0 $sub->($self);
976             }
977 123         223 return $self;
978             }
979              
980 217     217   381 sub run ($self, $name, @args) {
  217         361  
  217         413  
  217         430  
  217         318  
981 217         943 $self->call_name($name);
982 217         1504 $self->collect(@args);
983 217         914 $self->commit;
984 217         88215 $self->validate;
985 217         724 my ($child, @child_args) = $self->find_child;
986 213 100       838 return $child->run(@child_args) if defined $child;
987              
988             # we're the executors
989 123         726 $self->execution_reason($child_args[0]);
990 123         715 $self->final_collect; # no @args passed in this collection
991 123         547 $self->final_commit;
992 123         616 $self->pre_execute_run;
993 123         10017 return $self->execute;
994             } ## end sub run
995              
996             package App::Easer::V2::Command::Commands;
997             push our @ISA, 'App::Easer::V2::Command';
998 35     35   126 sub aliases { 'commands' }
999 14     14   37 sub allow_residual_options { 0 }
1000 1     1   5 sub description { 'Print list of supported sub-commands' }
1001 20     20   55 sub help { 'list sub-commands' }
1002 0     0   0 sub name { 'commands' }
1003              
1004 80     80   131 sub target ($self) {
  80         130  
  80         114  
1005 80         197 my ($subc, @rest) = $self->residual_args;
1006 80 50       218 die "this command does not support many arguments\n" if @rest;
1007 80         226 my $target = $self->parent;
1008 80 100       386 $target = $target->find_matching_child($subc) if defined $subc;
1009 80 50       275 die "cannot find sub-command '$subc'\n" unless defined $target;
1010 80         403 return $target;
1011             } ## end sub target ($self)
1012              
1013 41     41   76 sub list_commands_for ($self, $target = undef) {
  41         74  
  41         136  
  41         52  
1014 41   33     171 $target //= $self->target;
1015 41         75 my @lines;
1016 41         149 for my $command ($target->inflate_children($target->list_children)) {
1017 69   100     254 my $help = $command->help // '(**missing help**)';
1018 69         189 my @aliases = $command->aliases;
1019 69 50       164 next unless @aliases;
1020 69         234 push @lines, sprintf '%15s: %s', shift(@aliases), $help;
1021 69 100       276 push @lines, sprintf '%15s (also as: %s)', '', join ', ', @aliases
1022             if @aliases;
1023             } ## end for my $command ($target...)
1024 41 100       561 return unless @lines;
1025 19         135 return join "\n", @lines;
1026             } ## end sub list_commands_for
1027              
1028 40     40   75 sub _build_printout_facility ($self) {
  40         65  
  40         64  
1029 40         104 my $channel = $self->target->help_channel;
1030 40         224 my $refch = ref $channel;
1031              
1032 40 50       130 return $channel if $refch eq 'CODE';
1033              
1034 40         99 my $fh;
1035 40 50       210 if ($refch eq 'GLOB') {
    50          
    50          
1036 0         0 $fh = $channel;
1037             }
1038             elsif ($refch eq 'SCALAR') {
1039 0 0       0 open $fh, '>', $channel or die "open(): $!\n";
1040             }
1041             elsif ($refch) {
1042 0         0 die 'invalid channel';
1043             }
1044             else {
1045 40         182 ($channel, my $binmode) = split m{:}mxs, $channel, 2;
1046 40 50 33     271 if ($channel eq '-' || lc($channel) eq '-stdout') {
    0          
1047 40         178 $fh = \*STDOUT;
1048             }
1049             elsif (lc($channel) eq '-stderr') {
1050 0         0 $fh = \*STDERR;
1051             }
1052             else {
1053 0 0       0 open $fh, '>', $channel or die "open('$channel'): $!\n";
1054             }
1055 40 50 50 17   1531 binmode $fh, $binmode if length($binmode // '');
  17         129  
  17         33  
  17         138  
1056             }
1057              
1058 40     40   73 return sub ($cmd, @stuff) {
  40         103  
  40         91  
  40         63  
1059 40         127 print {$fh} @stuff;
  40         3415  
1060 40         308 return $cmd;
1061             }
1062 40         21117 }
1063              
1064 40     40   162 sub printout ($self, @stuff) {
  40         161  
  40         101  
  40         65  
1065 40         111 my $pof = $self->_rw;
1066 40 50       303 $self->_rw($pof = $self->_build_printout_facility) unless $pof;
1067 40         247 $pof->($self, @stuff);
1068             }
1069              
1070 7     7   17 sub execute ($self) {
  7         17  
  7         13  
1071 7         30 my $target = $self->target;
1072 7   33     28 my $name = $target->call_name // $target->name;
1073 7 100       70 if (defined(my $commands = $self->list_commands_for($target))) {
1074 6         68 $self->printout("sub-commands for $name\n", $commands, "\n");
1075             }
1076             else {
1077 1         6 $self->printout("no sub-commands for $name\n");
1078             }
1079             } ## end sub execute ($self)
1080              
1081             package App::Easer::V2::Command::Help;
1082             push our @ISA, 'App::Easer::V2::Command::Commands';
1083             our @aliases = qw< help usage >;
1084 64     64   334 sub aliases { @aliases }
1085 66     66   167 sub allow_residual_options { 0 }
1086 1     1   5 sub description { 'Print help for (sub)command' }
1087 20     20   70 sub help { 'print a help command' }
1088 0     0   0 sub name { 'help' }
1089              
1090 49     49   157 sub __commandline_help ($getopt) {
  49         95  
  49         96  
1091 49         79 my @retval;
1092              
1093 49         160 my ($mode, $type, $desttype, $min, $max, $default);
1094 49 100       604 if (substr($getopt, -1, 1) eq '!') {
    100          
    100          
    100          
    100          
1095 15         29 $type = 'bool-negatable';
1096 15         39 substr $getopt, -1, 1, '';
1097 15         38 push @retval, 'boolean (can be negated)';
1098             }
1099             elsif ($getopt =~ s<:\+ ([@%])? \z><>mxs) {
1100 1         1 $mode = 'optional';
1101 1         2 $type = 'i';
1102 1         2 $default = 'increment';
1103 1         2 $desttype = $1;
1104 1         1 my $line = "integer, value is optional, defaults to incrementing current value";
1105 1 50 33     14 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
1106 1         2 push @retval, $line;
1107             } ## end elsif ($getopt =~ s<:+ ([@%])? \z><>mxs)
1108             elsif (substr($getopt, -1, 1) eq '+') {
1109 1         2 $mode = 'increment';
1110 1         2 substr $getopt, -1, 1, '';
1111 1         1 push @retval,
1112             'incremental integer (adds 1 every time it is provided)';
1113             } ## end elsif (substr($getopt, -1...))
1114             elsif (
1115             $getopt =~ s<(
1116             [:=]) # 1 mode
1117             ([siof]) # 2 type
1118             ([@%])? # 3 desttype
1119             (?:
1120             \{
1121             (\d*)? # 4 min
1122             ,?
1123             (\d*)? # 5 max
1124             \}
1125             )? \z><>mxs
1126             )
1127             {
1128 18 100       69 $mode = $1 eq '=' ? 'required' : 'optional';
1129 18         52 $type = $2;
1130 18         117 $desttype = $3;
1131 18         49 $min = $4;
1132 18         35 $max = $5;
1133 18 50       57 if (defined $min) {
1134 0 0       0 $mode = $min ? 'optional' : 'required';
1135             }
1136             $type = {
1137             s => 'string',
1138             i => 'integer',
1139             o => 'perl-extended-integer',
1140             f => 'float',
1141 18         165 }->{$type};
1142 18         68 my $line = "$type, value is $mode";
1143 18 50 33     128 $line .= ", at least $min times" if defined($min) && $min > 1;
1144 18 50 33     86 $line .= ", no more than $max times"
1145             if defined($max) && length($max);
1146 18 100 66     118 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
1147 18         87 push @retval, $line;
1148             } ## end elsif ($getopt =~ s<( ) )
1149             elsif ($getopt =~ s<: (\d+) ([@%])? \z><>mxs) {
1150 1         3 $mode = 'optional';
1151 1         2 $type = 'i';
1152 1         6 $default = $1;
1153 1         73 $desttype = $2;
1154 1         7 my $line = "integer, value is optional, defaults to $default";
1155 1 50 33     5 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
1156 1         4 push @retval, $line;
1157             } ## end elsif ($getopt =~ s<: (\d+) ([@%])? \z><>mxs)
1158             else { # boolean, non-negatable
1159 13         28 $type = 'bool';
1160 13         28 push @retval, 'boolean';
1161             }
1162              
1163 49         234 my @alternatives = split /\|/, $getopt;
1164 49 100 100     439 if ($type eq 'bool-negatable') {
    100          
    100          
1165             push @retval, map {
1166 15 100       35 if (length($_) == 1) { "-$_" }
  29         63  
  13         58  
1167 16         77 else { "--$_ | --no-$_" }
1168             } @alternatives;
1169             } ## end if ($type eq 'bool')
1170             elsif ($type eq 'bool' || $mode eq 'increment') {
1171             push @retval, map {
1172 14 100       43 if (length($_) == 1) { "-$_" }
  15         52  
  1         4  
1173 14         60 else { "--$_" }
1174             } @alternatives;
1175             } ## end if ($type eq 'bool')
1176             elsif ($mode eq 'optional') {
1177             push @retval, map {
1178 3 50       11 if (length($_) == 1) { "-$_ []" }
  3         7  
  0         0  
1179 3         10 else { "--$_ []" }
1180             } @alternatives;
1181             } ## end elsif ($mode eq 'optional')
1182             else {
1183             push @retval, map {
1184 17 100       155 if (length($_) == 1) { "-$_ " }
  30         77  
  13         44  
1185 17         52 else { "--$_ " }
1186             } @alternatives;
1187             } ## end else [ if ($type eq 'bool') ]
1188              
1189 49         192 return @retval;
1190             } ## end sub __commandline_help ($getopt)
1191              
1192 33     33   91 sub execute ($self) {
  33         54  
  33         68  
1193 33         190 $self->printout($self->collect_help_for($self->target, $self->call_name));
1194 33         522 return 0;
1195             }
1196              
1197 49     49   134 sub real_environment_variable_name ($self, $target, $opt) {
  49         79  
  49         185  
  49         73  
  49         61  
1198 49         122 my $name = $target->name_for_option($opt);
1199 49         136 while ($target) {
1200             return $target->environment_variable_name($opt)
1201 58 100 66     358 if $opt && ! $opt->{inherited};
1202              
1203             # here, either the option is not available at this level (which is
1204             # in any case not possible, as inheritance is always from the direct
1205             # parent) or it's inherited. Move on to the parent to look for the
1206             # real source of info.
1207 9   50     27 $target = $target->parent // die 'weird inheritance, uh?!?';
1208             ($opt) = # search the parent's options, very inefficient but still...
1209 9         40 grep { $target->name_for_option($_) eq $name } $target->options;
  23         56  
1210             }
1211             # we should never arrive here for real options
1212 0         0 return;
1213             }
1214              
1215 34     34   65 sub collect_help_for ($self, $target, $mode = 'help') {
  34         57  
  34         60  
  34         70  
  34         60  
1216 34         58 my @stuff;
1217              
1218 26     26   50 my $trim_and_prefix = sub ($text, $prefix = ' ') {
  26         56  
  26         62  
  26         48  
1219 26         237 $text =~ s{\A\s+|\s+\z}{}gmxs; # trim
1220 26         176 $text =~ s{^}{$prefix}gmxs; # add some indentation
1221 26         125 return $text;
1222 34         185 };
1223              
1224 34   100     147 push @stuff, ($target->help // 'no concise help yet'), "\n\n";
1225              
1226 34 100 66     239 if ($mode eq 'help' && defined(my $description = $target->description)) {
1227 26         92 push @stuff, "Description:\n", $trim_and_prefix->($description), "\n\n";
1228             }
1229              
1230 34 50       144 if (defined(my $usage = $target->usage)) {
1231 0         0 push @stuff, "Usage:\n", $trim_and_prefix->($usage), "\n\n";
1232             }
1233              
1234             # Print this only for sub-commands, not for the root
1235 34 100       109 push @stuff, sprintf "Can be called as: %s\n\n", join ', ',
1236             $target->aliases
1237             if $target->parent;
1238              
1239 34         160 my @options = $target->options;
1240 34         197 my $options_help = $target->options_help;
1241 34 100 66     239 if (@options || defined($options_help)) {
1242 28         83 push @stuff, "Options:\n";
1243              
1244 28   50     172 $options_help //= {};
1245 28 50       178 if (! ref($options_help)) {
1246 0         0 push @stuff, $trim_and_prefix->($options_help), "\n\n";
1247             }
1248             else {
1249 28   50     207 my $preamble = $options_help->{preamble} // undef;
1250 28 50       87 push @stuff, $trim_and_prefix->($preamble), "\n\n"
1251             if defined($preamble);
1252              
1253 28         59 my $n = 0; # count the option
1254 28         90 for my $opt (@options) {
1255 49 100       232 push @stuff, "\n" if $n++; # from second line on
1256              
1257             push @stuff, sprintf "%15s: %s\n", $target->name_for_option($opt),
1258 49   100     202 $opt->{help} // '';
1259              
1260 49 50       156 if (exists $opt->{getopt}) {
1261 49         252 my @lines = __commandline_help($opt->{getopt});
1262 49         249 push @stuff, sprintf "%15s command-line: %s\n", '',
1263             shift(@lines);
1264             push @stuff,
1265 49         99 map { sprintf "%15s %s\n", '', $_ } @lines;
  77         287  
1266             } ## end if (exists $opt->{getopt...})
1267              
1268 49 100       161 if (defined(my $env = $self->real_environment_variable_name($target, $opt))) {
1269 20         95 push @stuff, sprintf "%15s environment: %s\n", '', $env;
1270             }
1271              
1272 49 100       229 if (exists($opt->{default})) {
1273 17         99 my $default = $opt->{default};
1274             my $print = ! defined($default) ? '*undef*'
1275             : ! ref($default) ? $default
1276 17 100       143 : do { require JSON::PP; JSON::PP::encode_json($default) };
  1 50       9  
  1         10  
1277 17         211 push @stuff, sprintf "%15s default: %s\n", '', $print;
1278             }
1279             } ## end for my $opt (@options)
1280              
1281 28   50     133 my $postamble = $options_help->{postamble} // undef;
1282 28 50       92 push @stuff, "\n", $trim_and_prefix->($postamble), "\n"
1283             if defined($postamble);
1284             }
1285              
1286 28         63 push @stuff, "\n";
1287             } ## end if (my @options = $target...)
1288             else {
1289 6         16 push @stuff, "This command has no option\n";
1290             }
1291              
1292 34 100       244 if (defined(my $commands = $self->list_commands_for($target))) {
1293 13         42 push @stuff, "Sub-commands:\n", $commands, "\n";
1294             }
1295             else {
1296 21         58 push @stuff, "No sub-commands\n";
1297             }
1298              
1299 34         625 return join '', @stuff;
1300             } ## end sub execute ($self)
1301              
1302             package App::Easer::V2::Command::Tree;
1303             push our @ISA, 'App::Easer::V2::Command::Commands';
1304 27     27   72 sub aliases { 'tree' }
1305 1     1   6 sub description { 'Print tree of supported sub-commands' }
1306 20     20   49 sub help { 'print sub-commands in a tree' }
1307 0     0   0 sub name { 'tree' }
1308              
1309             sub options {
1310             return (
1311             {
1312 1     1   8 getopt => 'include_auto|include-auto|I!',
1313             default => 0,
1314             environment => 1,
1315             },
1316             );
1317             } ## end sub options
1318              
1319 0     0   0 sub list_commands_for ($self, $target) {
  0         0  
  0         0  
  0         0  
1320 0 0       0 my $exclude_auto = $self->config('include_auto') ? 0 : 1;
1321 0         0 my @lines;
1322 0         0 for my $command ($target->inflate_children($target->list_children)) {
1323 0 0       0 my ($name) = $command->aliases or next;
1324             next
1325 0 0 0     0 if $name =~ m{\A(?: help | commands | tree)\z}mxs && $exclude_auto;
1326 0   0     0 my $help = $command->help // '(**missing help**)';
1327 0         0 push @lines, sprintf '- %s (%s)', $name, $help;
1328 0 0       0 if (defined(my $subtree = $self->list_commands_for($command))) {
1329 0         0 push @lines, $subtree =~ s{^}{ }rgmxs;
1330             }
1331             } ## end for my $command ($target...)
1332 0 0       0 return unless @lines;
1333 0         0 return join "\n", @lines;
1334             } ## end sub list_commands_for
1335              
1336             1;