File Coverage

blib/lib/App/Easer/V2.pm
Criterion Covered Total %
statement 717 816 87.8
branch 177 284 62.3
condition 61 119 51.2
subroutine 103 114 90.3
pod 4 4 100.0
total 1062 1337 79.4


line stmt bran cond sub pod time code
1             package App::Easer::V2;
2 20     20   146648 use v5.24;
  20         71  
3 20     20   86 use warnings;
  20         32  
  20         612  
4 20     20   473 use experimental qw< signatures >;
  20         2882  
  20         90  
5 20     20   1957 no warnings qw< experimental::signatures >;
  20         31  
  20         1037  
6             { our $VERSION = '2.007' }
7 20     20   102 use Carp;
  20         40  
  20         1257  
8              
9 20     20   1231 use parent 'Exporter';
  20         791  
  20         120  
10             our @EXPORT_OK = qw< appeaser_api d dd run >;
11              
12             # repeated stuff to ease direct usage and fatpack-like inclusion
13 2     2 1 3509 sub appeaser_api { __PACKAGE__ =~ s{.*::}{}rmxs }
14 0     0 1 0 sub d { warn dd(@_) }
15              
16 0     0 1 0 sub dd (@stuff) {
  0         0  
  0         0  
17 20     20   3264 no warnings;
  20         51  
  20         4869  
18 0         0 require Data::Dumper;
19 0         0 local $Data::Dumper::Indent = 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 87     87 1 346360 sub run ($app, @args) {
  87         163  
  87         244  
  87         132  
28 87         149 my $class = 'App::Easer::V2::Command';
29 87 0       730 my $instance =
    50          
30             ref($app) eq 'HASH' ? $class->new($app)
31             : ref($app) eq 'ARRAY' ? $class->instantiate($app->@*)
32             : $class->instantiate($app);
33 87         294 return $instance->run(@args);
34             } ## end sub run
35              
36 8     8   4185 sub import ($package, @args) {
  8         15  
  8         14  
  8         10  
37 8         20 my $target = caller;
38 8         30 my @args_for_exporter;
39 8         10 our %registered;
40 8         27 while (@args) {
41 14         28 my $request = shift @args;
42 14 100       44 if ($request eq '-command') {
    50          
    50          
    100          
43 6         13 $registered{$target} = 1;
44 20     20   130 no strict 'refs';
  20         30  
  20         1563  
45 6         7 push @{$target . '::ISA'}, 'App::Easer::V2::Command';
  6         100  
46             }
47             elsif ($request eq '-inherit') {
48 20     20   104 no strict 'refs';
  20         31  
  20         1804  
49 0         0 push @{$target . '::ISA'}, 'App::Easer::V2::Command';
  0         0  
50             }
51             elsif ($request eq '-register') {
52 0         0 $registered{$target} = 1;
53             }
54             elsif ($request eq '-spec') {
55 4 50       10 Carp::croak "no specification provided"
56             unless @args;
57 4 50       11 Carp::croak "invalid specification provided"
58             unless ref($args[0]) eq 'HASH';
59 20     20   103 no strict 'refs';
  20         37  
  20         507  
60 20     20   97 no warnings 'once';
  20         64  
  20         2605  
61 4         5 ${$target . '::app_easer_spec'} = shift @args;
  4         26  
62             } ## end elsif ($request eq '-spec')
63 4         11 else { push @args_for_exporter, $request }
64             } ## end while (@args)
65 8         296526 $package->export_to_level(1, $package, @args_for_exporter);
66             } ## end sub import
67              
68             package App::Easer::V2::Command;
69 20     20   112 use Scalar::Util 'blessed';
  20         30  
  20         813  
70 20     20   90 use List::Util 'any';
  20         34  
  20         2024  
71 20     20   8175 use English '-no_match_vars';
  20         30402  
  20         91  
72              
73             # some stuff can be managed via a hash reference kept in a "slot",
74             # allowing for overriding should be easy either with re-defining the
75             # "slot" method, or overriding the sub-method relying on it. The name of
76             # the slot is the same as the name of the actual package that $self is
77             # blessed into.
78 6352   50 6352   6141 sub slot ($self) { return $self->{blessed($self)} //= {} }
  6352         6337  
  6352         5945  
  6352         21915  
79              
80             # This is a poor man's way to easily define attributes in a single line
81             # Corinna will be a blessing eventually
82 6352     6352   28408 sub _rwn ($self, $name, @newval) {
  6352         6613  
  6352         6850  
  6352         6286  
  6352         5942  
83 6352         8248 my $vref = \$self->slot->{$name};
84 6352 100       11531 $$vref = $newval[0] if @newval;
85 6352         14896 return $$vref;
86             }
87              
88 3008     3008   3065 sub _rw ($s, @n) { $s->_rwn((caller(1))[3] =~ s{.*::}{}rmxs, @n) }
  3008         3152  
  3008         3252  
  3008         2896  
  3008         15971  
89              
90 2370     2370   2454 sub _rwa ($self, @n) {
  2370         2646  
  2370         2422  
  2370         2325  
91 2370         15694 my $aref = $self->_rwn((caller(1))[3] =~ s{.*::}{}rmxs, @n);
92 2370 50       5605 Carp::confess() unless defined $aref;
93 2370         4876 return $aref->@*;
94             }
95              
96 135     135   151 sub _rwad ($self, @n) {
  135         172  
  135         158  
  135         158  
97 135   50     997 my $aref = $self->_rwn((caller(1))[3] =~ s{.*::}{}rmxs, @n) // [];
98 135 50       599 return wantarray ? $aref->@* : [$aref->@*];
99             }
100              
101             # these "attributes" would point to stuff that is normally "scalar" and
102             # used as specification overall. It can be overridden but probably it's
103             # just easier to stick in a hash inside the slot. We don't want to put
104             # executables here, though - overriding should be the guiding principle
105             # in this case.
106 135     135   160 sub aliases ($self, @r) {
  135         154  
  135         150  
  135         147  
107 135 100       246 if (my @aliases = $self->_rwad(@r)) { return @aliases }
  111         460  
108 24 100       48 if (defined(my $name = $self->_rwn('name'))) { return $name }
  22         66  
109 2         31 return;
110             }
111 262     262   356 sub allow_residual_options ($self, @r) { $self->_rw(@r) }
  262         303  
  262         297  
  262         268  
  262         471  
112 108     108   121 sub auto_environment ($self, @r) { $self->_rw(@r) }
  108         120  
  108         138  
  108         121  
  108         170  
113 167     167   201 sub call_name ($self, @r) { $self->_rw(@r) }
  167         195  
  167         222  
  167         183  
  167         340  
114 442     442   482 sub children ($self, @r) { $self->_rwa(@r) }
  442         465  
  442         453  
  442         447  
  442         669  
115 884     884   1084 sub children_prefixes ($self, @r) { $self->_rwa(@r) }
  884         1038  
  884         909  
  884         891  
  884         1516  
116 52     52   69 sub default_child ($self, @r) { $self->_rw(@r) }
  52         64  
  52         61  
  52         53  
  52         91  
117 20     20   34 sub description ($self, @r) { $self->_rw(@r) }
  20         30  
  20         31  
  20         29  
  20         51  
118 102     102   109 sub environment_prefix ($self, @r) { $self->_rw(@r) }
  102         103  
  102         397  
  102         101  
  102         163  
119 87     87   129 sub execution_reason ($self, @r) { $self->_rw(@r) }
  87         111  
  87         163  
  87         138  
  87         185  
120 442     442   495 sub force_auto_children ($self, @r) { $self->_rw(@r) }
  442         557  
  442         465  
  442         438  
  442         781  
121 5     5   10 sub fallback_to ($self, @r) { $self->_rw(@r) }
  5         8  
  5         8  
  5         8  
  5         13  
122 186     186   224 sub hashy_class ($self, @r) { $self->_rw(@r) }
  186         204  
  186         200  
  186         206  
  186         277  
123 30     30   44 sub help ($self, @r) { $self->_rw(@r) }
  30         60  
  30         41  
  30         36  
  30         62  
124 29     29   54 sub help_channel ($slf, @r) { $slf->_rw(@r) }
  29         41  
  29         45  
  29         56  
  29         59  
125 35   100 35   464 sub name ($s, @r) { $s->_rw(@r) // ($s->aliases)[0] // '**no name**' }
  35   100     53  
  35         46  
  35         40  
  35         72  
126 160     160   187 sub params_validate ($self, @r) { $self->_rw(@r) }
  160         215  
  160         184  
  160         179  
  160         253  
127 579     579   666 sub parent ($self, @r) { $self->_rw(@r) }
  579         616  
  579         688  
  579         570  
  579         980  
128 364     364   532 sub residual_args ($self, @r) { $self->_rwa(@r) }
  364         417  
  364         409  
  364         388  
  364         559  
129 160     160   219 sub sources ($self, @r) { $self->_rwa(@r) }
  160         190  
  160         185  
  160         190  
  160         342  
130              
131 151     151   185 sub supports ($self, $what) {
  151         210  
  151         199  
  151         186  
132 151     165   837 any { $_ eq $what } $self->aliases;
  165         1069  
133             }
134              
135 520     520   583 sub options ($self, @r) {
  520         565  
  520         544  
  520         535  
136 520         866 return map { $self->resolve_options($_) } $self->_rwa(@r);
  969         1373  
137             }
138              
139 969     969   1004 sub resolve_options ($self, $spec) {
  969         996  
  969         947  
  969         895  
140 969 100       2191 return $spec if ref($spec) eq 'HASH';
141 18 50       38 $spec = [inherit_options => $spec] unless ref $spec;
142 18 50       55 Carp::confess("invalid spec $spec") unless ref($spec) eq 'ARRAY';
143 18         37 my ($method_name, @names) = $spec->@*;
144 18 50       50 my $method = $self->can($method_name)
145             or Carp::confess("cannot find method $method_name in $self");
146 18         33 return $self->$method(@names);
147             } ## end sub resolve_options
148              
149 18     18   19 sub inherit_options ($self, @names) {
  18         17  
  18         25  
  18         18  
150 18         19 my %got;
151             map {
152 18         26 my @options;
  18         18  
153 18 50       35 if ($_ eq '+parent') {
154 18   100     27 @options = grep { $_->{transmit} // 0 } $self->parent->options;
  108         191  
155             }
156             else {
157 0         0 my $namerx = qr{\A(?:$_)\z};
158 0         0 my $ancestor = $self->parent;
159 0         0 while ($ancestor) {
160             push @options, my @pass = # FIXME something's strange here
161             grep {
162 0         0 my $name = $self->name_for_option($_);
  0         0  
163             (!$_->{transmit_exact})
164             && $name =~ m{$namerx}
165 0 0 0     0 && !$got{$name};
166             } $ancestor->options;
167 0         0 $ancestor = $ancestor->parent;
168             } ## end while ($ancestor)
169             } ## end else [ if ($_ eq '+parent') ]
170 18         26 map { +{transmit => 1, $_->%*, inherited => 1} } @options;
  90         304  
171             } @names;
172             } ## end sub inherit_options
173              
174 307     307   401 sub new ($pkg, @args) {
  307         358  
  307         377  
  307         327  
175 20     20   32730 my $pkg_spec = do { no strict 'refs'; ${$pkg . '::app_easer_spec'} };
  20         302  
  20         126994  
  307         355  
  307         320  
  307         1037  
176 307 100 100     4433 my $slot = {
      66        
177             aliases => [],
178             allow_residual_options => 0,
179             auto_environment => 0,
180             children => [],
181             children_prefixes => [$pkg . '::Cmd'],
182             default_child => 'help',
183             environment_prefix => '',
184             fallback_to => undef,
185             force_auto_children => undef,
186             hashy_class => __PACKAGE__,
187             help_channel => '-STDOUT:encoding(UTF-8)',
188             options => [],
189             params_validate => undef,
190             sources => [qw< +CmdLine +Environment +Parent=70 +Default=100 >],
191             ($pkg_spec // {})->%*,
192             (@args && ref $args[0] ? $args[0]->%* : @args),
193             };
194 307         973 my $self = bless {$pkg => $slot}, $pkg;
195 307         659 return $self;
196             } ## end sub new
197              
198 660     660   732 sub merge_hashes ($self, @hrefs) {
  660         703  
  660         811  
  660         660  
199 660         778 my (%retval, %is_overridable);
200 660         966 for my $href (@hrefs) {
201 1705         2849 for my $src_key (keys $href->%*) {
202 953         1133 my $dst_key = $src_key;
203 953         968 my $this_overridable = 0;
204             $retval{$dst_key} = $href->{$src_key}
205 953 100 66     2800 if $is_overridable{$dst_key} || !exists($retval{$dst_key});
206 953 50       1885 $is_overridable{$dst_key} = 0 unless $this_overridable;
207             } ## end for my $src_key (keys $href...)
208             } ## end for my $href (@hrefs)
209 660         1316 return \%retval;
210             } ## end sub merge_hashes
211              
212             # collect options values from $args (= [...]) & other sources
213             # sets own configuration and residual_args
214             # acts based on what is provided by method options()
215 160     160   201 sub collect ($self, @args) {
  160         200  
  160         232  
  160         183  
216 160         229 my @sequence; # stuff collected from Sources, w/ context
217             my @slices; # ditto, no context
218 160         234 my $config = {}; # merged configuration
219 160         205 my @residual_args; # what is left from the @args at the end
220              
221 160         215 my $last_priority = 0;
222 160         379 for my $source ($self->sources) {
223 660 100       1473 my ($src, @opts) = ref($source) eq 'ARRAY' ? $source->@* : $source;
224 660 100 100     1453 my $meta = (@opts && ref $opts[0]) ? shift @opts : {};
225 660         852 my $locator = $src;
226 660 100       1081 if (! ref($src)) {
227 658         1541 ($src, my $priority) = split m{=}mxs, $src;
228 658 100       1339 $meta->{priority} = $priority if defined $priority;
229 658         2904 $locator = $src =~ s{\A \+}{source_}rmxs;
230             }
231 660 50       1424 my $sub = $self->ref_to_sub($locator)
232             or die "unhandled source for $locator\n";
233 660         1501 my ($slice, $residuals) = $sub->($self, \@opts, \@args);
234 660 100       11529 push @residual_args, $residuals->@* if defined $residuals;
235 660   66     1775 $last_priority = my $priority = $meta->{priority} //= $last_priority + 10;
236 660         1415 push @sequence, [$priority, $src, \@opts, $locator, $slice];
237 660         1472 for (my $i = $#sequence; $i > 0; --$i) {
238 540 100       1466 last if $sequence[$i - 1][0] <= $sequence[$i][0];
239 41         111 @sequence[$i - 1, $i] = @sequence[$i, $i - 1];
240             }
241 660         932 $config = $self->merge_hashes(map {$_->[-1]} @sequence);
  1705         2784  
242 660         1971 $self->_rwn(config => {merged => $config, sequence => \@sequence});
243             } ## end for my $source ($self->...)
244              
245             # save and return
246 160         506 $self->residual_args(\@residual_args);
247 160         307 return $self;
248             } ## end sub collect
249              
250 160     160   221 sub getopt_config ($self, @n) {
  160         196  
  160         193  
  160         181  
251 160         308 my $value = $self->_rw(@n);
252 160 50       410 if (!defined $value) {
253 160         310 my @r = qw< gnu_getopt >;
254 160 100       399 push @r, qw< require_order pass_through > if $self->list_children;
255 160 50       407 push @r, qw< pass_through > if $self->allow_residual_options;
256 160         316 $value = $self->_rw(\@r);
257             } ## end if (!defined $value)
258 160         644 return $value->@*;
259             } ## end sub getopt_config
260              
261 160     160   192 sub source_CmdLine ($self, $ignore, $args) {
  160         203  
  160         210  
  160         218  
  160         183  
262 160         496 my @args = $args->@*;
263              
264 160         10187 require Getopt::Long;
265 160         129523 Getopt::Long::Configure('default', $self->getopt_config);
266              
267 160         9104 my %option_for;
268             my @specs = map {
269 300         400 my $go = $_->{getopt};
270             ref($go) eq 'ARRAY'
271 0     0   0 ? ($go->[0] => sub { $go->[1]->(\%option_for, @_) })
272 300 50       590 : $go;
273             }
274 160         466 grep { exists $_->{getopt} } $self->options;
  300         593  
275 160 50       494 Getopt::Long::GetOptionsFromArray(\@args, \%option_for, @specs)
276             or die "bailing out\n";
277              
278             # Check if we want to forbid the residual @args to start with a '-'
279 160         34113 my $strict = !$self->allow_residual_options;
280 160 50 66     853 die "bailing out (allow_residual_options is false and got <@args>)"
      66        
281             if $strict && @args && $args[0] =~ m{\A - . }mxs;
282              
283 160         486 return (\%option_for, \@args);
284             } ## end sub source_CmdLine
285              
286 295     295   322 sub name_for_option ($self, $o) {
  295         367  
  295         308  
  295         280  
287 295 50       547 return $o->{name} if defined $o->{name};
288             return $1
289 295 50 33     2098 if defined $o->{getopt} && $o->{getopt} =~ m{\A(\w[-\w]*)}mxs;
290             return lc $o->{environment}
291 0 0 0     0 if defined $o->{environment} && $o->{environment} ne '1';
292 0         0 return '~~~';
293             } ## end sub name_for_option
294              
295 160     160   193 sub source_Default ($self, @ignore) {
  160         205  
  160         216  
  160         170  
296             return {
297 128         218 map { $self->name_for_option($_) => $_->{default} }
298 270         429 grep { exists $_->{default} }
299 160         359 grep { !$_->{inherited} } $self->options
  300         546  
300             };
301             } ## end sub source_Default
302              
303 3     3   6 sub source_FromTrail ($self, $trail, @ignore) {
  3         4  
  3         3  
  3         6  
  3         4  
304 3         6 my $conf = $self->config_hash;
305 3         8 for my $key ($trail->@*) {
306 9 50       15 return {} unless defined $conf->{$key};
307 9         11 $conf = $conf->{$key};
308 9 50       20 die "invalid trail $trail->@* for configuration gathering"
309             unless ref($conf) eq 'HASH';
310             } ## end for my $key ($keys->@*)
311 3         7 return $conf;
312             }
313              
314 304     304   339 sub environment_variable_name ($self, $ospec) {
  304         349  
  304         320  
  304         310  
315             my $env =
316             exists $ospec->{environment} ? $ospec->{environment}
317 304 100       752 : $self->auto_environment ? 1
    100          
318             : undef;
319 304 100 100     924 return $env unless ($env // '') eq '1';
320              
321             # get prefixes all the way up to the first command
322 93         99 my @prefixes;
323 93         161 for (my $instance = $self; $instance; $instance = $instance->parent) {
324 102   50     212 unshift @prefixes, $instance->environment_prefix // '';
325             }
326              
327 93         195 return uc(join '', @prefixes, $self->name_for_option($ospec));
328             } ## end sub environment_variable_name
329              
330 160     160   193 sub source_Environment ($self, @ignore) {
  160         204  
  160         218  
  160         178  
331             return {
332             map {
333 270         466 my $en = $self->environment_variable_name($_);
334             defined($en)
335             && exists($ENV{$en})
336 270 100 100     1131 ? ($self->name_for_option($_) => $ENV{$en})
337             : ();
338 160         332 } grep { !$_->{inherited} } $self->options
  300         577  
339             };
340             } ## end sub source_Environment
341              
342 16     16   24 sub source_JsonFileFromConfig ($self, $key, @ignore) {
  16         23  
  16         18  
  16         26  
  16         19  
343 16   50     76 $key = $key->[0] // 'config';
344 16 100       48 defined(my $filename = $self->config($key)) or return {};
345 9         1273 require JSON::PP;
346 9         24428 return JSON::PP::decode_json($self->slurp($filename));
347             } ## end sub source_JsonFileFromConfig
348              
349 9     9   14 sub slurp ($self, $file, $mode = '<:encoding(UTF-8)') {
  9         17  
  9         10  
  9         17  
  9         12  
350 9 50       358 open my $fh, $mode, $file or die "open('$file'): $!\n";
351 9         19208 local $/;
352 9         339 return <$fh>;
353             }
354              
355 0     0   0 sub source_JsonFiles ($self, $candidates, @ignore) {
  0         0  
  0         0  
  0         0  
  0         0  
356 0         0 require JSON::PP;
357             return $self->merge_hashes(
358 0         0 map { JSON::PP::decode_json($self->slurp($_)) }
359 0         0 grep { -e $_ } $candidates->@*
  0         0  
360             );
361             } ## end sub source_JsonFiles
362              
363 158     158   198 sub source_Parent ($self, @ignore) {
  158         195  
  158         214  
  158         169  
364 158 100       338 my $parent = $self->parent or return {};
365 73         202 return $parent->config_hash(0);
366             }
367              
368              
369             # get the assembled config for the command. It supports the optional
370             # additional boolean parameter $blame to get back a more structured
371             # version where it's clear where each option comes from, to allow for
372             # further injection of parameters from elsewhere.
373 155     155   358 sub config_hash ($self, $blame = 0) {
  155         205  
  155         208  
  155         176  
374 155   50     253 my $config = $self->_rwn('config') // {};
375 155 50       265 return $config if $blame;
376 155   50     616 return $config->{merged} // {};
377             }
378              
379             # get one or more specific configurtion values
380 32     32   104 sub config ($self, @keys) {
  32         48  
  32         54  
  32         34  
381 32         67 my $hash = $self->config_hash(0);
382 32 50       129 return $hash->{$keys[0]} if @keys == 1;
383 0         0 return $hash->@{@keys};
384             }
385              
386 0     0   0 sub set_config ($self, $key, @value) {
  0         0  
  0         0  
  0         0  
  0         0  
387 0         0 my $hash = $self->config_hash(0);
388 0         0 delete $hash->{$key};
389 0 0       0 $hash->{$key} = $value[0] if @value;
390 0         0 return $self;
391             } ## end sub set_config
392              
393             # commit collected options values, called after collect ends
394 160     160   187 sub commit ($self, @n) {
  160         203  
  160         177  
  160         169  
395 160         284 my $commit = $self->_rw(@n);
396 160 50       312 return $commit if @n;
397 160 50       338 return unless $commit;
398 0         0 return $self->ref_to_sub($commit)->($self);
399             } ## end sub commit
400              
401             # validate collected options values, called after commit ends.
402 160     160   190 sub validate ($self, @n) {
  160         189  
  160         178  
  160         169  
403              
404             # Support the "accessor" interface for using a validation sub
405 160         245 my $validator = $self->_rw(@n);
406 160 50       335 return $validator if @n;
407              
408             # If set, it MUST be a validation sub reference. Otherwise, try the
409             # params_validate/Params::Validate path.
410 160 50       496 if ($validator) {
    50          
411 0 0       0 die "validator can only be a CODE reference\n"
412             unless ref $validator eq 'CODE';
413 0         0 $validator->($self);
414             }
415             elsif (my $params_validate = $self->params_validate) {
416 0         0 require Params::Validate;
417 0 0 0     0 if (my $config_validator = $params_validate->{config} // undef) {
418 0         0 my @array = $self->config_hash;
419 0         0 Params::Validate::validate(\@array, $config_validator);
420             }
421 0 0 0     0 if (my $args_validator = $params_validate->{args} // undef) {
422 0         0 my @array = $self->residual_args;
423 0         0 Params::Validate::validate_pos(\@array, $args_validator->@*);
424             }
425             }
426             else {} # no validation needed
427              
428 160         249 return $self;
429             } ## end sub validate ($self)
430              
431 92     92   125 sub find_matching_child ($self, $command) {
  92         119  
  92         149  
  92         146  
432 92 50       196 return unless defined $command;
433 92         186 for my $candidate ($self->list_children) {
434 151         358 my ($child) = $self->inflate_children($candidate);
435 151 100       431 return $child if $child->supports($command);
436             }
437 5         20 return;
438             } ## end sub find_matching_child
439              
440 26     26   32 sub _inflate_default_child ($self) {
  26         35  
  26         31  
441 26 50       47 defined(my $default = $self->default_child)
442             or die "undefined default child\n";
443 26 100       110 return undef if $default eq '-self';
444 4 50       9 my $child = $self->find_matching_child($default)
445             or die "no child matching the default $default\n";
446 4         16 return $child;
447             } ## end sub inflate_default_child ($self)
448              
449             # look for a child to hand execution over. Returns an child instance or
450             # undef (which means that the $self is in charge of executing
451             # something). This implements the most sensible default, deviations will
452             # have to be coded explicitly.
453             # Return values:
454             # - (undef, '-leaf') if no child exists
455             # - ($instance, @args) if a child is found with $args[0]
456             # - ($instance, '-default') if the default child is returned
457             # - (undef, '-fallback') in case $self is the fallback
458             # - ($instance, '-fallback', @args) in case the fallback is returned
459 160     160   196 sub find_child ($self) {
  160         198  
  160         171  
460 160 100       281 my @candidates = $self->list_children or return (undef, '-leaf');
461 96         228 my @residuals = $self->residual_args;
462 96 100       253 if (@residuals) {
    50          
463 70 100       193 if (my $child = $self->find_matching_child($residuals[0])) {
464 65         268 return ($child, @residuals);
465             } # otherwise... see what the fallback is about
466             }
467             elsif (defined(my $default = $self->default_child)) {
468 26         70 return ($self->_inflate_default_child, '-default');
469             }
470              
471             # try the fallback...
472 5         20 my $fallback = $self->fallback;
473 5 100       17 if (defined $fallback) {
474 1 50       5 return (undef, '-fallback') if $fallback eq '-self';
475 0 0       0 return ($self->_inflate_default_child, '-default')
476             if $fallback eq '-default';
477 0 0       0 if (my $child = $self->find_matching_child($fallback)) {
478 0         0 return ($child, -fallback => @residuals);
479             }
480             } ## end if (defined $fallback)
481              
482             # no fallback at this point... it's an error, build a message and die!
483             # FIXME this can be improved
484 4         66 die "cannot find sub-command '$residuals[0]'\n";
485             } ## end sub find_child ($self)
486              
487             # get the list of children. This only gives back a list of "hints" that
488             # can be turned into instances via inflate_children. In this case, it's
489             # module names
490 442     442   537 sub list_children ($self) {
  442         550  
  442         447  
491 442         770 my @children = $self->children;
492 442         1813 require File::Spec;
493             my @expanded_inc = map {
494 442         790 my ($v, $dirs) = File::Spec->splitpath($_, 'no-file');
  4067         12898  
495 4067         17163 [$v, File::Spec->splitdir($dirs)];
496             } @INC;
497 442         664 my %seen;
498             push @children, map {
499 442         963 my @parts = split m{::}mxs, $_ . 'x';
  442         1309  
500 442         904 substr(my $bprefix = pop @parts, -1, 1, '');
501             map {
502 442         664 my ($v, @dirs) = $_->@*;
  4067         10667  
503 4067         24294 my $dirs = File::Spec->catdir(@dirs, @parts);
504 4067 50       57385 if (opendir my $dh, File::Spec->catpath($v, $dirs, '')) {
505 0         0 grep { !$seen{$_}++ }
506             map {
507 0         0 substr(my $lastpart = $_, -3, 3, '');
508 0         0 join '::', @parts, $lastpart;
509             } grep {
510 0         0 my $path = File::Spec->catpath($v, $dirs, $_);
511 0 0 0     0 (-e $path && !-d $path)
      0        
512             && substr($_, 0, length($bprefix)) eq $bprefix
513             && substr($_, -3, 3) eq '.pm'
514 0         0 } sort { $a cmp $b } readdir $dh;
  0         0  
515             } ## end if (opendir my $dh, File::Spec...)
516 4067         17135 else { () }
517             } @expanded_inc;
518             } $self->children_prefixes;
519             push @children, map {
520 442         1102 my $prefix = $_;
  442         559  
521 44         157 grep { !$seen{$_}++ }
522             grep {
523 442         888 my $this_prefix = substr $_, 0, length $prefix;
  423         599  
524 423         805 $this_prefix eq $prefix;
525             } keys %App::Easer::V2::registered;
526             } $self->children_prefixes;
527 442 100 100     969 push @children, $self->auto_children
528             if $self->force_auto_children // @children;
529 442         3349 return @children;
530             } ## end sub list_children ($self)
531              
532 911     911   877 sub _auto_child ($self, $name, $inflate = 0) {
  911         918  
  911         924  
  911         928  
  911         851  
533 911         1729 my $child = __PACKAGE__ . '::' . ucfirst(lc($name));
534 911 100       1392 ($child) = $self->inflate_children($child) if $inflate;
535 911         1740 return $child;
536             }
537              
538             # returns either class names or inflated objects
539 302     302   387 sub auto_children ($self, $inflate = 0) {
  302         368  
  302         356  
  302         537  
540 302         463 map { $self->_auto_child($_, $inflate) } qw< help commands tree >;
  906         1284  
541             }
542              
543 1     1   6 sub auto_commands ($self) { return $self->_auto_child('commands', 1) }
  1         1  
  1         2  
  1         3  
544              
545 4     4   11 sub auto_help ($self) { return $self->_auto_child('help', 1) }
  4         5  
  4         6  
  4         14  
546              
547 0     0   0 sub auto_tree ($self) { return $self->_auto_child('tree', 1) }
  0         0  
  0         0  
  0         0  
548              
549 2     2   12 sub run_help ($self) { return $self->auto_help->run($self->name) }
  2         3  
  2         4  
  2         8  
550 1     1   57 sub full_help_text ($s) { return $s->auto_help->collect_help_for($s) }
  1         2  
  1         2  
  1         4  
551              
552 0     0   0 sub load_module ($sop, $module) {
  0         0  
  0         0  
  0         0  
553 0         0 my $file = "$module.pm" =~ s{::}{/}grmxs;
554 0 0       0 eval { require $file } or Carp::confess("module<$module>: $EVAL_ERROR");
  0         0  
555 0         0 return $module;
556             }
557              
558             # Gets a specification like "Foo::Bar::baz" and returns a reference to
559             # sub "baz" in "Foo::Bar". If no package name is set, returns a
560             # reference to a sub in the package of $self. FIXME document properly
561 706     706   771 sub ref_to_sub ($self, $spec) {
  706         743  
  706         840  
  706         706  
562 706 50       1086 Carp::confess("undefined specification in ref_to_sub")
563             unless defined $spec;
564 706 100       1189 return $spec if ref($spec) eq 'CODE';
565 658 50       3493 my ($class, $function) =
566             ref($spec) eq 'ARRAY'
567             ? $spec->@*
568             : $spec =~ m{\A (?: (.*) :: )? (.*) \z}mxs;
569 658 100 100     4264 return $self->can($function) unless length($class // '');
570 1 50       12 $self->load_module($class) unless $class->can($function);
571 1         5 return $class->can($function);
572             } ## end sub ref_to_sub
573              
574 220     220   292 sub instantiate ($sop, $class, @args) {
  220         245  
  220         244  
  220         247  
  220         238  
575 220 50       1629 $sop->load_module($class) unless $class->can('new');
576 220         479 return $class->new(@args);
577             }
578              
579             # transform one or more children "hints" into instances.
580 186     186   229 sub inflate_children ($self, @hints) {
  186         220  
  186         250  
  186         184  
581 186         360 my $hashy = $self->hashy_class;
582             map {
583 220         280 my $child = $_;
584 220 50       464 if (!blessed($child)) { # actually inflate it
585 220 100       723 $child =
    100          
586             ref($child) eq 'ARRAY' ? $self->instantiate($child->@*)
587             : ref($child) eq 'HASH' ? $self->instantiate($hashy, $child)
588             : $self->instantiate($child);
589             } ## end if (!blessed($child))
590 220         607 $child->parent($self);
591 220         732 $child;
592 186         354 } grep { defined $_ } @hints;
  220         412  
593             } ## end sub inflate_children
594              
595             # fallback mechanism when finding a child, relies on fallback_to.
596 5     5   9 sub fallback ($self) {
  5         12  
  5         7  
597 5         16 my $fto = $self->fallback_to;
598 5 50 66     30 return $fto if !defined($fto) || $fto !~ m{\A(?: 0 | [1-9]\d* )\z};
599 0         0 my @children = $self->list_children;
600 0 0       0 return $children[$fto] if $fto <= $#children;
601 0         0 return undef;
602             } ## end sub fallback ($self)
603              
604             # execute what's set as the execute sub in the slot
605 46     46   86 sub execute ($self) {
  46         409  
  46         59  
606 46 50       79 my $spec = $self->_rw or die "nothing to search for execution\n";
607 46 50       124 my $sub = $self->ref_to_sub($spec) or die "nothing to execute\n";
608 46         195 return $sub->($self);
609             }
610              
611 160     160   229 sub run ($self, $name, @args) {
  160         221  
  160         219  
  160         226  
  160         192  
612 160         466 $self->call_name($name);
613 160         655 $self->collect(@args);
614 160         461 $self->commit;
615 160         439 $self->validate;
616 160         376 my ($child, @child_args) = $self->find_child;
617 156 100       564 return $child->run(@child_args) if defined $child;
618 87         383 $self->execution_reason($child_args[0]);
619 87         327 return $self->execute;
620             } ## end sub run
621              
622             package App::Easer::V2::Command::Commands;
623             push our @ISA, 'App::Easer::V2::Command';
624 34     34   88 sub aliases { 'commands' }
625 14     14   33 sub allow_residual_options { 0 }
626 1     1   3 sub description { 'Print list of supported sub-commands' }
627 19     19   44 sub help { 'list sub-commands' }
628 0     0   0 sub name { 'commands' }
629              
630 58     58   105 sub target ($self) {
  58         66  
  58         70  
631 58         139 my ($subc, @rest) = $self->residual_args;
632 58 50       130 die "this command does not support many arguments\n" if @rest;
633 58         159 my $target = $self->parent;
634 58 100       152 $target = $target->find_matching_child($subc) if defined $subc;
635 58 50       130 die "cannot find sub-command '$subc'\n" unless defined $target;
636 58         209 return $target;
637             } ## end sub target ($self)
638              
639 30     30   52 sub list_commands_for ($self, $target = undef) {
  30         46  
  30         101  
  30         39  
640 30   33     122 $target //= $self->target;
641 30         49 my @lines;
642 30         104 for my $command ($target->inflate_children($target->list_children)) {
643 64   50     165 my $help = $command->help // '(**missing help**)';
644 64         142 my @aliases = $command->aliases;
645 64 50       132 next unless @aliases;
646 64         187 push @lines, sprintf '%15s: %s', shift(@aliases), $help;
647 64 100       159 push @lines, sprintf '%15s (also as: %s)', '', join ', ', @aliases
648             if @aliases;
649             } ## end for my $command ($target...)
650 30 100       297 return unless @lines;
651 18         99 return join "\n", @lines;
652             } ## end sub list_commands_for
653              
654 29     29   73 sub _build_printout_facility ($self) {
  29         61  
  29         42  
655 29         71 my $channel = $self->target->help_channel;
656 29         99 my $refch = ref $channel;
657              
658 29 50       84 return $channel if $refch eq 'CODE';
659              
660 29         50 my $fh;
661 29 50       153 if ($refch eq 'GLOB') {
    50          
    50          
662 0         0 $fh = $channel;
663             }
664             elsif ($refch eq 'SCALAR') {
665 0 0       0 open $fh, '>', $channel or die "open(): $!\n";
666             }
667             elsif ($refch) {
668 0         0 die 'invalid channel';
669             }
670             else {
671 29         113 ($channel, my $binmode) = split m{:}mxs, $channel, 2;
672 29 50 33     188 if ($channel eq '-' || lc($channel) eq '-stdout') {
    0          
673 29         172 $fh = \*STDOUT;
674             }
675             elsif (lc($channel) eq '-stderr') {
676 0         0 $fh = \*STDERR;
677             }
678             else {
679 0 0       0 open $fh, '>', $channel or die "open('$channel'): $!\n";
680             }
681 29 50 50 13   767 binmode $fh, $binmode if length($binmode // '');
  13         75  
  13         21  
  13         106  
682             }
683              
684 29     29   42 return sub ($cmd, @stuff) {
  29         49  
  29         50  
  29         52  
685 29         41 print {$fh} @stuff;
  29         1853  
686 29         189 return $cmd;
687             }
688 29         109765 }
689              
690 29     29   46 sub printout ($self, @stuff) {
  29         48  
  29         87  
  29         52  
691 29         70 my $pof = $self->_rw;
692 29 50       160 $self->_rw($pof = $self->_build_printout_facility) unless $pof;
693 29         128 $pof->($self, @stuff);
694             }
695              
696 7     7   14 sub execute ($self) {
  7         16  
  7         12  
697 7         23 my $target = $self->target;
698 7   33     23 my $name = $target->call_name // $target->name;
699 7 100       41 if (defined(my $commands = $self->list_commands_for($target))) {
700 6         35 $self->printout("sub-commands for $name\n", $commands, "\n");
701             }
702             else {
703 1         6 $self->printout("no sub-commands for $name\n");
704             }
705             } ## end sub execute ($self)
706              
707             package App::Easer::V2::Command::Help;
708             push our @ISA, 'App::Easer::V2::Command::Commands';
709 55     55   235 sub aliases { 'help' }
710 44     44   123 sub allow_residual_options { 0 }
711 1     1   4 sub description { 'Print help for (sub)command' }
712 19     19   53 sub help { 'print a help command' }
713 0     0   0 sub name { 'help' }
714              
715 34     34   48 sub __commandline_help ($getopt) {
  34         50  
  34         39  
716 34         39 my @retval;
717              
718 34         55 my ($mode, $type, $desttype, $min, $max, $default);
719 34 100       259 if (substr($getopt, -1, 1) eq '!') {
    100          
    100          
    100          
    100          
720 15         42 $type = 'bool-negatable';
721 15         29 substr $getopt, -1, 1, '';
722 15         29 push @retval, 'boolean (can be negated)';
723             }
724             elsif ($getopt =~ s<:\+ ([@%])? \z><>mxs) {
725 1         3 $mode = 'optional';
726 1         2 $type = 'i';
727 1         2 $default = 'increment';
728 1         2 $desttype = $1;
729 1         2 my $line = "integer, value is optional, defaults to incrementing current value";
730 1 50 33     4 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
731 1         2 push @retval, $line;
732             } ## end elsif ($getopt =~ s<:+ ([@%])? \z><>mxs)
733             elsif (substr($getopt, -1, 1) eq '+') {
734 1         2 $mode = 'increment';
735 1         2 substr $getopt, -1, 1, '';
736 1         2 push @retval,
737             'incremental integer (adds 1 every time it is provided)';
738             } ## end elsif (substr($getopt, -1...))
739             elsif (
740             $getopt =~ s<(
741             [:=]) # 1 mode
742             ([siof]) # 2 type
743             ([@%])? # 3 desttype
744             (?:
745             \{
746             (\d*)? # 4 min
747             ,?
748             (\d*)? # 5 max
749             \}
750             )? \z><>mxs
751             )
752             {
753 15 100       67 $mode = $1 eq '=' ? 'required' : 'optional';
754 15         47 $type = $2;
755 15         33 $desttype = $3;
756 15         45 $min = $4;
757 15         28 $max = $5;
758 15 50       39 if (defined $min) {
759 0 0       0 $mode = $min ? 'optional' : 'required';
760             }
761             $type = {
762             s => 'string',
763             i => 'integer',
764             o => 'perl-extended-integer',
765             f => 'float',
766 15         90 }->{$type};
767 15         60 my $line = "$type, value is $mode";
768 15 50 33     48 $line .= ", at least $min times" if defined($min) && $min > 1;
769 15 50 33     46 $line .= ", no more than $max times"
770             if defined($max) && length($max);
771 15 100 66     53 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
772 15         32 push @retval, $line;
773             } ## end elsif ($getopt =~ s<( ) )
774             elsif ($getopt =~ s<: (\d+) ([@%])? \z><>mxs) {
775 1         2 $mode = 'optional';
776 1         2 $type = 'i';
777 1         2 $default = $1;
778 1         2 $desttype = $2;
779 1         2 my $line = "integer, value is optional, defaults to $default";
780 1 50 33     11 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
781 1         3 push @retval, $line;
782             } ## end elsif ($getopt =~ s<: (\d+) ([@%])? \z><>mxs)
783             else { # boolean, non-negatable
784 1         2 $type = 'bool';
785 1         2 push @retval, 'boolean';
786             }
787              
788 34         123 my @alternatives = split /\|/, $getopt;
789 34 100 100     247 if ($type eq 'bool-negatable') {
    100          
    100          
790             push @retval, map {
791 15 100       39 if (length($_) == 1) { "-$_" }
  29         57  
  13         43  
792 16         55 else { "--$_ | --no-$_" }
793             } @alternatives;
794             } ## end if ($type eq 'bool')
795             elsif ($type eq 'bool' || $mode eq 'increment') {
796             push @retval, map {
797 2 100       6 if (length($_) == 1) { "-$_" }
  3         7  
  1         4  
798 2         13 else { "--$_" }
799             } @alternatives;
800             } ## end if ($type eq 'bool')
801             elsif ($mode eq 'optional') {
802             push @retval, map {
803 3 50       5 if (length($_) == 1) { "-$_ []" }
  3         6  
  0         0  
804 3         7 else { "--$_ []" }
805             } @alternatives;
806             } ## end elsif ($mode eq 'optional')
807             else {
808             push @retval, map {
809 14 100       32 if (length($_) == 1) { "-$_ " }
  27         70  
  13         33  
810 14         42 else { "--$_ " }
811             } @alternatives;
812             } ## end else [ if ($type eq 'bool') ]
813              
814 34         107 return @retval;
815             } ## end sub __commandline_help ($getopt)
816              
817 22     22   76 sub execute ($self) {
  22         61  
  22         33  
818 22         114 $self->printout($self->collect_help_for($self->target));
819 22         842 return 0;
820             }
821              
822 23     23   36 sub collect_help_for ($self, $target = undef) {
  23         61  
  23         42  
  23         30  
823 23   33     103 $target //= $self->target;
824 23         37 my @stuff;
825              
826 23   100     81 push @stuff, ($target->help // 'no concise help yet'), "\n\n";
827              
828 23 100       97 if (defined(my $description = $target->description)) {
829 21         130 $description =~ s{\A\s+|\s+\z}{}gmxs; # trim
830 21         83 $description =~ s{^}{ }gmxs; # add some indentation
831 21         69 push @stuff, "Description:\n$description\n\n";
832             }
833              
834             # Print this only for sub-commands, not for the root
835 23 100       76 push @stuff, sprintf "Can be called as: %s\n\n", join ', ',
836             $target->aliases
837             if $target->parent;
838              
839 23 100       78 if (my @options = $target->options) {
840 17         40 push @stuff, "Options:\n";
841 17         28 my $n = 0; # count the option
842 17         41 for my $opt (@options) {
843 34 100       99 push @stuff, "\n" if $n++; # from second line on
844              
845             push @stuff, sprintf "%15s: %s\n", $target->name_for_option($opt),
846 34   100     107 $opt->{help} // '';
847              
848 34 50       106 if (exists $opt->{getopt}) {
849 34         98 my @lines = __commandline_help($opt->{getopt});
850 34         141 push @stuff, sprintf "%15s command-line: %s\n", '',
851             shift(@lines);
852             push @stuff,
853 34         90 map { sprintf "%15s %s\n", '', $_ } @lines;
  62         245  
854             } ## end if (exists $opt->{getopt...})
855              
856 34 100       124 if (defined(my $env = $self->environment_variable_name($opt))) {
857 14         51 push @stuff, sprintf "%15s environment: %s\n", '', $env;
858             }
859              
860             push @stuff, sprintf "%15s default: %s\n", '',
861             $opt->{default} // '*undef*'
862 34 100 50     147 if exists $opt->{default};
863             } ## end for my $opt (@options)
864              
865 17         45 push @stuff, "\n";
866             } ## end if (my @options = $target...)
867             else {
868 6         16 push @stuff, "This command has no option\n";
869             }
870              
871 23 100       120 if (defined(my $commands = $self->list_commands_for($target))) {
872 12         46 push @stuff, "Sub-commands:\n", $commands, "\n";
873             }
874             else {
875 11         29 push @stuff, "No sub-commands\n";
876             }
877              
878 23         282 return join '', @stuff;
879             } ## end sub execute ($self)
880              
881             package App::Easer::V2::Command::Tree;
882             push our @ISA, 'App::Easer::V2::Command::Commands';
883 26     26   55 sub aliases { 'tree' }
884 1     1   3 sub description { 'Print tree of supported sub-commands' }
885 19     19   42 sub help { 'print sub-commands in a tree' }
886 0     0   0 sub name { 'tree' }
887              
888             sub options {
889             return (
890             {
891 1     1   7 getopt => 'include_auto|include-auto|I!',
892             default => 0,
893             environment => 1,
894             },
895             );
896             } ## end sub options
897              
898 0     0   0 sub list_commands_for ($self, $target) {
  0         0  
  0         0  
  0         0  
899 0 0       0 my $exclude_auto = $self->config('include_auto') ? 0 : 1;
900 0         0 my @lines;
901 0         0 for my $command ($target->inflate_children($target->list_children)) {
902 0 0       0 my ($name) = $command->aliases or next;
903             next
904 0 0 0     0 if $name =~ m{\A(?: help | commands | tree)\z}mxs && $exclude_auto;
905 0   0     0 my $help = $command->help // '(**missing help**)';
906 0         0 push @lines, sprintf '- %s (%s)', $name, $help;
907 0 0       0 if (defined(my $subtree = $self->list_commands_for($command))) {
908 0         0 push @lines, $subtree =~ s{^}{ }rgmxs;
909             }
910             } ## end for my $command ($target...)
911 0 0       0 return unless @lines;
912 0         0 return join "\n", @lines;
913             } ## end sub list_commands_for
914              
915             1;