File Coverage

blib/lib/App/Easer/V2.pm
Criterion Covered Total %
statement 718 818 87.7
branch 177 284 62.3
condition 61 119 51.2
subroutine 103 114 90.3
pod 4 4 100.0
total 1063 1339 79.3


line stmt bran cond sub pod time code
1             package App::Easer::V2;
2 20     20   175769 use v5.24;
  20         87  
3 20     20   107 use warnings;
  20         40  
  20         724  
4 20     20   578 use experimental qw< signatures >;
  20         3497  
  20         112  
5 20     20   2350 no warnings qw< experimental::signatures >;
  20         38  
  20         1640  
6             { our $VERSION = '2.007001' }
7 20     20   131 use Carp;
  20         36  
  20         1204  
8              
9 20     20   1500 use parent 'Exporter';
  20         878  
  20         133  
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 3348 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   3695 no warnings;
  20         62  
  20         5845  
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 391050 sub run ($app, @args) {
  87         197  
  87         252  
  87         130  
28 87         154 my $class = 'App::Easer::V2::Command';
29 87 0       724 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         297 return $instance->run(@args);
34             } ## end sub run
35              
36 8     8   4937 sub import ($package, @args) {
  8         18  
  8         19  
  8         11  
37 8         24 my $target = caller;
38 8         40 my @args_for_exporter;
39 8         13 our %registered;
40 8         25 while (@args) {
41 14         28 my $request = shift @args;
42 14 100       62 if ($request eq '-command') {
    50          
    50          
    100          
43 6         13 $registered{$target} = 1;
44 20     20   187 no strict 'refs';
  20         40  
  20         1817  
45 6         9 push @{$target . '::ISA'}, 'App::Easer::V2::Command';
  6         67  
46             }
47             elsif ($request eq '-inherit') {
48 20     20   126 no strict 'refs';
  20         50  
  20         2238  
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       20 Carp::croak "invalid specification provided"
58             unless ref($args[0]) eq 'HASH';
59 20     20   132 no strict 'refs';
  20         39  
  20         652  
60 20     20   108 no warnings 'once';
  20         64  
  20         3030  
61 4         7 ${$target . '::app_easer_spec'} = shift @args;
  4         29  
62             } ## end elsif ($request eq '-spec')
63 4         12 else { push @args_for_exporter, $request }
64             } ## end while (@args)
65 8         353182 $package->export_to_level(1, $package, @args_for_exporter);
66             } ## end sub import
67              
68             package App::Easer::V2::Command;
69 20     20   137 use Scalar::Util 'blessed';
  20         57  
  20         957  
70 20     20   117 use List::Util 'any';
  20         37  
  20         1995  
71 20     20   9758 use English '-no_match_vars';
  20         35680  
  20         111  
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   7739 sub slot ($self) { return $self->{blessed($self)} //= {} }
  6352         7723  
  6352         7418  
  6352         25512  
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   34738 sub _rwn ($self, $name, @newval) {
  6352         7954  
  6352         8600  
  6352         7793  
  6352         7377  
83 6352         10244 my $vref = \$self->slot->{$name};
84 6352 100       14336 $$vref = $newval[0] if @newval;
85 6352         17881 return $$vref;
86             }
87              
88 3008     3008   4431 sub _rw ($s, @n) { $s->_rwn((caller(1))[3] =~ s{.*::}{}rmxs, @n) }
  3008         3889  
  3008         3862  
  3008         3748  
  3008         18863  
89              
90 2370     2370   3102 sub _rwa ($self, @n) {
  2370         3149  
  2370         2994  
  2370         2995  
91 2370         18504 my $aref = $self->_rwn((caller(1))[3] =~ s{.*::}{}rmxs, @n);
92 2370 50       6828 Carp::confess() unless defined $aref;
93 2370         5737 return $aref->@*;
94             }
95              
96 135     135   219 sub _rwad ($self, @n) {
  135         185  
  135         196  
  135         161  
97 135   50     1050 my $aref = $self->_rwn((caller(1))[3] =~ s{.*::}{}rmxs, @n) // [];
98 135 50       661 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   240 sub aliases ($self, @r) {
  135         192  
  135         165  
  135         165  
107 135 100       269 if (my @aliases = $self->_rwad(@r)) { return @aliases }
  111         482  
108 24 100       49 if (defined(my $name = $self->_rwn('name'))) { return $name }
  22         72  
109 2         40 return;
110             }
111 262     262   393 sub allow_residual_options ($self, @r) { $self->_rw(@r) }
  262         369  
  262         360  
  262         319  
  262         530  
112 108     108   146 sub auto_environment ($self, @r) { $self->_rw(@r) }
  108         179  
  108         141  
  108         135  
  108         214  
113 167     167   211 sub call_name ($self, @r) { $self->_rw(@r) }
  167         234  
  167         281  
  167         207  
  167         365  
114 442     442   551 sub children ($self, @r) { $self->_rwa(@r) }
  442         570  
  442         928  
  442         546  
  442         819  
115 884     884   1610 sub children_prefixes ($self, @r) { $self->_rwa(@r) }
  884         1213  
  884         1125  
  884         1118  
  884         1811  
116 52     52   71 sub default_child ($self, @r) { $self->_rw(@r) }
  52         74  
  52         70  
  52         63  
  52         111  
117 20     20   44 sub description ($self, @r) { $self->_rw(@r) }
  20         33  
  20         35  
  20         29  
  20         58  
118 102     102   128 sub environment_prefix ($self, @r) { $self->_rw(@r) }
  102         135  
  102         137  
  102         117  
  102         171  
119 87     87   145 sub execution_reason ($self, @r) { $self->_rw(@r) }
  87         129  
  87         188  
  87         129  
  87         187  
120 442     442   577 sub force_auto_children ($self, @r) { $self->_rw(@r) }
  442         607  
  442         556  
  442         540  
  442         837  
121 5     5   10 sub fallback_to ($self, @r) { $self->_rw(@r) }
  5         10  
  5         8  
  5         9  
  5         13  
122 186     186   253 sub hashy_class ($self, @r) { $self->_rw(@r) }
  186         248  
  186         245  
  186         233  
  186         414  
123 30     30   77 sub help ($self, @r) { $self->_rw(@r) }
  30         47  
  30         44  
  30         39  
  30         86  
124 29     29   58 sub help_channel ($slf, @r) { $slf->_rw(@r) }
  29         69  
  29         51  
  29         53  
  29         76  
125 35   100 35   408 sub name ($s, @r) { $s->_rw(@r) // ($s->aliases)[0] // '**no name**' }
  35   100     58  
  35         52  
  35         48  
  35         85  
126 160     160   225 sub params_validate ($self, @r) { $self->_rw(@r) }
  160         255  
  160         288  
  160         191  
  160         306  
127 579     579   766 sub parent ($self, @r) { $self->_rw(@r) }
  579         782  
  579         768  
  579         694  
  579         1142  
128 364     364   683 sub residual_args ($self, @r) { $self->_rwa(@r) }
  364         491  
  364         508  
  364         452  
  364         740  
129 160     160   226 sub sources ($self, @r) { $self->_rwa(@r) }
  160         216  
  160         219  
  160         197  
  160         371  
130              
131 151     151   250 sub supports ($self, $what) {
  151         231  
  151         303  
  151         245  
132 151     165   715 any { $_ eq $what } $self->aliases;
  165         1252  
133             }
134              
135 520     520   738 sub options ($self, @r) {
  520         689  
  520         671  
  520         635  
136 520         988 return map { $self->resolve_options($_) } $self->_rwa(@r);
  969         1701  
137             }
138              
139 969     969   1187 sub resolve_options ($self, $spec) {
  969         1188  
  969         1217  
  969         1131  
140 969 100       2703 return $spec if ref($spec) eq 'HASH';
141 18 50       48 $spec = [inherit_options => $spec] unless ref $spec;
142 18 50       42 Carp::confess("invalid spec $spec") unless ref($spec) eq 'ARRAY';
143 18         39 my ($method_name, @names) = $spec->@*;
144 18 50       60 my $method = $self->can($method_name)
145             or Carp::confess("cannot find method $method_name in $self");
146 18         41 return $self->$method(@names);
147             } ## end sub resolve_options
148              
149 18     18   23 sub inherit_options ($self, @names) {
  18         22  
  18         26  
  18         21  
150 18         27 my %got;
151             map {
152 18         27 my @options;
  18         22  
153 18 50       37 if ($_ eq '+parent') {
154 18   100     38 @options = grep { $_->{transmit} // 0 } $self->parent->options;
  108         234  
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         38 map { +{transmit => 1, $_->%*, inherited => 1} } @options;
  90         344  
171             } @names;
172             } ## end sub inherit_options
173              
174 307     307   476 sub new ($pkg, @args) {
  307         470  
  307         416  
  307         408  
175 20     20   42708 my $pkg_spec = do { no strict 'refs'; ${$pkg . '::app_easer_spec'} };
  20         327  
  20         152776  
  307         436  
  307         380  
  307         1147  
176 307 100 100     5059 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         1131 my $self = bless {$pkg => $slot}, $pkg;
195 307         726 return $self;
196             } ## end sub new
197              
198 660     660   943 sub merge_hashes ($self, @hrefs) {
  660         867  
  660         956  
  660         817  
199 660         900 my (%retval, %is_overridable);
200 660         1185 for my $href (@hrefs) {
201 1705         3925 for my $src_key (keys $href->%*) {
202 953         1282 my $dst_key = $src_key;
203 953         1165 my $this_overridable = 0;
204             $retval{$dst_key} = $href->{$src_key}
205 953 100 66     3182 if $is_overridable{$dst_key} || !exists($retval{$dst_key});
206 953 50       2334 $is_overridable{$dst_key} = 0 unless $this_overridable;
207             } ## end for my $src_key (keys $href...)
208             } ## end for my $href (@hrefs)
209 660         1562 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   229 sub collect ($self, @args) {
  160         231  
  160         260  
  160         199  
216 160         287 my @sequence; # stuff collected from Sources, w/ context
217             my @slices; # ditto, no context
218 160         268 my $config = {}; # merged configuration
219 160         256 my @residual_args; # what is left from the @args at the end
220              
221 160         257 my $last_priority = 0;
222 160         393 for my $source ($self->sources) {
223 660 100       1762 my ($src, @opts) = ref($source) eq 'ARRAY' ? $source->@* : $source;
224 660 100 100     1726 my $meta = (@opts && ref $opts[0]) ? shift @opts : {};
225 660         1003 my $locator = $src;
226 660 100       1300 if (! ref($src)) {
227 658         1884 ($src, my $priority) = split m{=}mxs, $src;
228 658 100       1631 $meta->{priority} = $priority if defined $priority;
229 658         2462 $locator = $src =~ s{\A \+}{source_}rmxs;
230             }
231 660 50       1605 my $sub = $self->ref_to_sub($locator)
232             or die "unhandled source for $locator\n";
233 660         1729 my ($slice, $residuals) = $sub->($self, \@opts, \@args);
234 660 100       14215 push @residual_args, $residuals->@* if defined $residuals;
235 660   66     2081 $last_priority = my $priority = $meta->{priority} //= $last_priority + 10;
236 660         1729 push @sequence, [$priority, $src, \@opts, $locator, $slice];
237 660         1785 for (my $i = $#sequence; $i > 0; --$i) {
238 540 100       1779 last if $sequence[$i - 1][0] <= $sequence[$i][0];
239 41         124 @sequence[$i - 1, $i] = @sequence[$i, $i - 1];
240             }
241 660         1240 $config = $self->merge_hashes(map {$_->[-1]} @sequence);
  1705         3244  
242 660         2064 $self->_rwn(config => {merged => $config, sequence => \@sequence});
243             } ## end for my $source ($self->...)
244              
245             # save and return
246 160         593 $self->residual_args(\@residual_args);
247 160         373 return $self;
248             } ## end sub collect
249              
250 160     160   226 sub getopt_config ($self, @n) {
  160         236  
  160         226  
  160         196  
251 160         325 my $value = $self->_rw(@n);
252 160 50       479 if (!defined $value) {
253 160         359 my @r = qw< gnu_getopt >;
254 160 100       419 push @r, qw< require_order pass_through > if $self->list_children;
255 160 50       440 push @r, qw< pass_through > if $self->allow_residual_options;
256 160         357 $value = $self->_rw(\@r);
257             } ## end if (!defined $value)
258 160         715 return $value->@*;
259             } ## end sub getopt_config
260              
261 160     160   303 sub source_CmdLine ($self, $ignore, $args) {
  160         230  
  160         230  
  160         217  
  160         214  
262 160         570 my @args = $args->@*;
263              
264 160         12092 require Getopt::Long;
265 160         155656 Getopt::Long::Configure('default', $self->getopt_config);
266              
267 160         10390 my %option_for;
268             my @specs = map {
269 300         467 my $go = $_->{getopt};
270             ref($go) eq 'ARRAY'
271 0     0   0 ? ($go->[0] => sub { $go->[1]->(\%option_for, @_) })
272 300 50       675 : $go;
273             }
274 160         468 grep { exists $_->{getopt} } $self->options;
  300         638  
275 160 50       541 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         38270 my $strict = !$self->allow_residual_options;
280 160 50 66     959 die "bailing out (allow_residual_options is false and got <@args>)"
      66        
281             if $strict && @args && $args[0] =~ m{\A - . }mxs;
282              
283 160         546 return (\%option_for, \@args);
284             } ## end sub source_CmdLine
285              
286 295     295   395 sub name_for_option ($self, $o) {
  295         388  
  295         368  
  295         372  
287 295 50       607 return $o->{name} if defined $o->{name};
288             return $1
289 295 50 33     2519 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   231 sub source_Default ($self, @ignore) {
  160         226  
  160         242  
  160         217  
296             return {
297 128         251 map { $self->name_for_option($_) => $_->{default} }
298 270         541 grep { exists $_->{default} }
299 160         350 grep { !$_->{inherited} } $self->options
  300         603  
300             };
301             } ## end sub source_Default
302              
303 3     3   5 sub source_FromTrail ($self, $trail, @ignore) {
  3         6  
  3         5  
  3         5  
  3         5  
304 3         7 my $conf = $self->config_hash;
305 3         11 for my $key ($trail->@*) {
306 9 50       22 return {} unless defined $conf->{$key};
307 9         12 $conf = $conf->{$key};
308 9 50       70 die "invalid trail $trail->@* for configuration gathering"
309             unless ref($conf) eq 'HASH';
310             } ## end for my $key ($keys->@*)
311 3         9 return $conf;
312             }
313              
314 304     304   426 sub environment_variable_name ($self, $ospec) {
  304         402  
  304         404  
  304         373  
315             my $env =
316             exists $ospec->{environment} ? $ospec->{environment}
317 304 100       734 : $self->auto_environment ? 1
    100          
318             : undef;
319 304 100 100     1026 return $env unless ($env // '') eq '1';
320              
321             # get prefixes all the way up to the first command
322 93         132 my @prefixes;
323 93         205 for (my $instance = $self; $instance; $instance = $instance->parent) {
324 102   50     194 unshift @prefixes, $instance->environment_prefix // '';
325             }
326              
327 93         212 return uc(join '', @prefixes, $self->name_for_option($ospec));
328             } ## end sub environment_variable_name
329              
330 160     160   229 sub source_Environment ($self, @ignore) {
  160         217  
  160         252  
  160         210  
331             return {
332             map {
333 270         610 my $en = $self->environment_variable_name($_);
334             defined($en)
335             && exists($ENV{$en})
336 270 100 100     1243 ? ($self->name_for_option($_) => $ENV{$en})
337             : ();
338 160         332 } grep { !$_->{inherited} } $self->options
  300         657  
339             };
340             } ## end sub source_Environment
341              
342 16     16   24 sub source_JsonFileFromConfig ($self, $key, @ignore) {
  16         26  
  16         25  
  16         25  
  16         26  
343 16   50     64 $key = $key->[0] // 'config';
344 16 100       44 defined(my $filename = $self->config($key)) or return {};
345 9         1620 require JSON::PP;
346 9         28767 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         14  
  9         16  
  9         15  
  9         16  
350 9 50       413 open my $fh, $mode, $file or die "open('$file'): $!\n";
351 9         23051 local $/;
352 9         370 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   225 sub source_Parent ($self, @ignore) {
  158         232  
  158         236  
  158         203  
364 158 100       331 my $parent = $self->parent or return {};
365 73         258 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   427 sub config_hash ($self, $blame = 0) {
  155         198  
  155         247  
  155         205  
374 155   50     260 my $config = $self->_rwn('config') // {};
375 155 50       324 return $config if $blame;
376 155   50     724 return $config->{merged} // {};
377             }
378              
379             # get one or more specific configurtion values
380 32     32   124 sub config ($self, @keys) {
  32         45  
  32         55  
  32         41  
381 32         68 my $hash = $self->config_hash(0);
382 32 50       132 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   251 sub commit ($self, @n) {
  160         260  
  160         209  
  160         191  
395 160         316 my $commit = $self->_rw(@n);
396 160 50       379 return $commit if @n;
397 160 50       396 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   239 sub validate ($self, @n) {
  160         230  
  160         235  
  160         201  
403              
404             # Support the "accessor" interface for using a validation sub
405 160         301 my $validator = $self->_rw(@n);
406 160 50       399 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       566 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         276 return $self;
429             } ## end sub validate ($self)
430              
431 92     92   169 sub find_matching_child ($self, $command) {
  92         139  
  92         141  
  92         120  
432 92 50       205 return unless defined $command;
433 92         196 for my $candidate ($self->list_children) {
434 151         403 my ($child) = $self->inflate_children($candidate);
435 151 100       439 return $child if $child->supports($command);
436             }
437 5         25 return;
438             } ## end sub find_matching_child
439              
440 26     26   42 sub _inflate_default_child ($self) {
  26         42  
  26         35  
441 26 50       67 defined(my $default = $self->default_child)
442             or die "undefined default child\n";
443 26 100       136 return undef if $default eq '-self';
444 4 50       12 my $child = $self->find_matching_child($default)
445             or die "no child matching the default $default\n";
446 4         21 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   236 sub find_child ($self) {
  160         223  
  160         197  
460 160 100       336 my @candidates = $self->list_children or return (undef, '-leaf');
461 96         252 my @residuals = $self->residual_args;
462 96 100       304 if (@residuals) {
    50          
463 70 100       204 if (my $child = $self->find_matching_child($residuals[0])) {
464 65         366 return ($child, @residuals);
465             } # otherwise... see what the fallback is about
466             }
467             elsif (defined(my $default = $self->default_child)) {
468 26         72 return ($self->_inflate_default_child, '-default');
469             }
470              
471             # try the fallback...
472 5         22 my $fallback = $self->fallback;
473 5 100       21 if (defined $fallback) {
474 1 50       7 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         75 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   633 sub list_children ($self) {
  442         646  
  442         599  
491 442         892 my @children = $self->children;
492              
493             # handle auto-loading of children from modules in @INC via prefixes
494 442         2127 require File::Spec;
495             my @expanded_inc = map {
496 442         955 my ($v, $dirs) = File::Spec->splitpath($_, 'no-file');
  4067         14713  
497 4067         20086 [$v, File::Spec->splitdir($dirs)];
498             } @INC;
499 442         778 my %seen;
500             my @autoloaded_children = map {
501 442         1087 my @parts = split m{::}mxs, $_ . 'x';
  442         1574  
502 442         1148 substr(my $bprefix = pop @parts, -1, 1, '');
503             map {
504 442         818 my ($v, @dirs) = $_->@*;
  4067         13393  
505 4067         28563 my $dirs = File::Spec->catdir(@dirs, @parts);
506 4067 50       68034 if (opendir my $dh, File::Spec->catpath($v, $dirs, '')) {
507 0         0 grep { !$seen{$_}++ }
508             map {
509 0         0 substr(my $lastpart = $_, -3, 3, '');
510 0         0 join '::', @parts, $lastpart;
511             } grep {
512 0         0 my $path = File::Spec->catpath($v, $dirs, $_);
513 0 0 0     0 (-e $path && !-d $path)
      0        
514             && substr($_, 0, length($bprefix)) eq $bprefix
515             && substr($_, -3, 3) eq '.pm'
516 0         0 } sort { $a cmp $b } readdir $dh;
  0         0  
517             } ## end if (opendir my $dh, File::Spec...)
518 4067         20501 else { () }
519             } @expanded_inc;
520             } $self->children_prefixes;
521             push @autoloaded_children, map {
522 442         1332 my $prefix = $_;
  442         652  
523 44         189 grep { !$seen{$_}++ }
524             grep {
525 442         1161 my $this_prefix = substr $_, 0, length $prefix;
  423         744  
526 423         892 $this_prefix eq $prefix;
527             } keys %App::Easer::V2::registered;
528             } $self->children_prefixes;
529              
530             # auto-loaded children are appended with consistent sorting
531 442         964 push @children, sort { $a cmp $b } @autoloaded_children;
  0         0  
532              
533 442 100 100     1111 push @children, $self->auto_children
534             if $self->force_auto_children // @children;
535 442         3993 return @children;
536             } ## end sub list_children ($self)
537              
538 911     911   1101 sub _auto_child ($self, $name, $inflate = 0) {
  911         1113  
  911         1112  
  911         1130  
  911         1077  
539 911         2150 my $child = __PACKAGE__ . '::' . ucfirst(lc($name));
540 911 100       1548 ($child) = $self->inflate_children($child) if $inflate;
541 911         1940 return $child;
542             }
543              
544             # returns either class names or inflated objects
545 302     302   447 sub auto_children ($self, $inflate = 0) {
  302         425  
  302         443  
  302         398  
546 302         481 map { $self->_auto_child($_, $inflate) } qw< help commands tree >;
  906         1598  
547             }
548              
549 1     1   5 sub auto_commands ($self) { return $self->_auto_child('commands', 1) }
  1         3  
  1         2  
  1         4  
550              
551 4     4   13 sub auto_help ($self) { return $self->_auto_child('help', 1) }
  4         7  
  4         6  
  4         14  
552              
553 0     0   0 sub auto_tree ($self) { return $self->_auto_child('tree', 1) }
  0         0  
  0         0  
  0         0  
554              
555 2     2   9 sub run_help ($self) { return $self->auto_help->run($self->name) }
  2         4  
  2         4  
  2         8  
556 1     1   46 sub full_help_text ($s) { return $s->auto_help->collect_help_for($s) }
  1         3  
  1         3  
  1         4  
557              
558 0     0   0 sub load_module ($sop, $module) {
  0         0  
  0         0  
  0         0  
559 0         0 my $file = "$module.pm" =~ s{::}{/}grmxs;
560 0 0       0 eval { require $file } or Carp::confess("module<$module>: $EVAL_ERROR");
  0         0  
561 0         0 return $module;
562             }
563              
564             # Gets a specification like "Foo::Bar::baz" and returns a reference to
565             # sub "baz" in "Foo::Bar". If no package name is set, returns a
566             # reference to a sub in the package of $self. FIXME document properly
567 706     706   953 sub ref_to_sub ($self, $spec) {
  706         915  
  706         997  
  706         836  
568 706 50       1438 Carp::confess("undefined specification in ref_to_sub")
569             unless defined $spec;
570 706 100       1462 return $spec if ref($spec) eq 'CODE';
571 658 50       3526 my ($class, $function) =
572             ref($spec) eq 'ARRAY'
573             ? $spec->@*
574             : $spec =~ m{\A (?: (.*) :: )? (.*) \z}mxs;
575 658 100 100     4748 return $self->can($function) unless length($class // '');
576 1 50       14 $self->load_module($class) unless $class->can($function);
577 1         6 return $class->can($function);
578             } ## end sub ref_to_sub
579              
580 220     220   305 sub instantiate ($sop, $class, @args) {
  220         295  
  220         300  
  220         332  
  220         329  
581 220 50       1792 $sop->load_module($class) unless $class->can('new');
582 220         547 return $class->new(@args);
583             }
584              
585             # transform one or more children "hints" into instances.
586 186     186   278 sub inflate_children ($self, @hints) {
  186         255  
  186         336  
  186         243  
587 186         357 my $hashy = $self->hashy_class;
588             map {
589 220         333 my $child = $_;
590 220 50       524 if (!blessed($child)) { # actually inflate it
591 220 100       815 $child =
    100          
592             ref($child) eq 'ARRAY' ? $self->instantiate($child->@*)
593             : ref($child) eq 'HASH' ? $self->instantiate($hashy, $child)
594             : $self->instantiate($child);
595             } ## end if (!blessed($child))
596 220         722 $child->parent($self);
597 220         917 $child;
598 186         399 } grep { defined $_ } @hints;
  220         522  
599             } ## end sub inflate_children
600              
601             # fallback mechanism when finding a child, relies on fallback_to.
602 5     5   10 sub fallback ($self) {
  5         11  
  5         10  
603 5         15 my $fto = $self->fallback_to;
604 5 50 66     44 return $fto if !defined($fto) || $fto !~ m{\A(?: 0 | [1-9]\d* )\z};
605 0         0 my @children = $self->list_children;
606 0 0       0 return $children[$fto] if $fto <= $#children;
607 0         0 return undef;
608             } ## end sub fallback ($self)
609              
610             # execute what's set as the execute sub in the slot
611 46     46   73 sub execute ($self) {
  46         76  
  46         65  
612 46 50       86 my $spec = $self->_rw or die "nothing to search for execution\n";
613 46 50       127 my $sub = $self->ref_to_sub($spec) or die "nothing to execute\n";
614 46         165 return $sub->($self);
615             }
616              
617 160     160   240 sub run ($self, $name, @args) {
  160         250  
  160         273  
  160         261  
  160         204  
618 160         513 $self->call_name($name);
619 160         757 $self->collect(@args);
620 160         502 $self->commit;
621 160         467 $self->validate;
622 160         455 my ($child, @child_args) = $self->find_child;
623 156 100       613 return $child->run(@child_args) if defined $child;
624 87         426 $self->execution_reason($child_args[0]);
625 87         378 return $self->execute;
626             } ## end sub run
627              
628             package App::Easer::V2::Command::Commands;
629             push our @ISA, 'App::Easer::V2::Command';
630 34     34   77 sub aliases { 'commands' }
631 14     14   67 sub allow_residual_options { 0 }
632 1     1   4 sub description { 'Print list of supported sub-commands' }
633 19     19   51 sub help { 'list sub-commands' }
634 0     0   0 sub name { 'commands' }
635              
636 58     58   96 sub target ($self) {
  58         82  
  58         81  
637 58         152 my ($subc, @rest) = $self->residual_args;
638 58 50       181 die "this command does not support many arguments\n" if @rest;
639 58         139 my $target = $self->parent;
640 58 100       165 $target = $target->find_matching_child($subc) if defined $subc;
641 58 50       152 die "cannot find sub-command '$subc'\n" unless defined $target;
642 58         218 return $target;
643             } ## end sub target ($self)
644              
645 30     30   54 sub list_commands_for ($self, $target = undef) {
  30         95  
  30         57  
  30         69  
646 30   33     84 $target //= $self->target;
647 30         52 my @lines;
648 30         145 for my $command ($target->inflate_children($target->list_children)) {
649 64   50     184 my $help = $command->help // '(**missing help**)';
650 64         163 my @aliases = $command->aliases;
651 64 50       141 next unless @aliases;
652 64         245 push @lines, sprintf '%15s: %s', shift(@aliases), $help;
653 64 100       179 push @lines, sprintf '%15s (also as: %s)', '', join ', ', @aliases
654             if @aliases;
655             } ## end for my $command ($target...)
656 30 100       363 return unless @lines;
657 18         115 return join "\n", @lines;
658             } ## end sub list_commands_for
659              
660 29     29   55 sub _build_printout_facility ($self) {
  29         55  
  29         41  
661 29         69 my $channel = $self->target->help_channel;
662 29         116 my $refch = ref $channel;
663              
664 29 50       83 return $channel if $refch eq 'CODE';
665              
666 29         62 my $fh;
667 29 50       141 if ($refch eq 'GLOB') {
    50          
    50          
668 0         0 $fh = $channel;
669             }
670             elsif ($refch eq 'SCALAR') {
671 0 0       0 open $fh, '>', $channel or die "open(): $!\n";
672             }
673             elsif ($refch) {
674 0         0 die 'invalid channel';
675             }
676             else {
677 29         120 ($channel, my $binmode) = split m{:}mxs, $channel, 2;
678 29 50 33     171 if ($channel eq '-' || lc($channel) eq '-stdout') {
    0          
679 29         85 $fh = \*STDOUT;
680             }
681             elsif (lc($channel) eq '-stderr') {
682 0         0 $fh = \*STDERR;
683             }
684             else {
685 0 0       0 open $fh, '>', $channel or die "open('$channel'): $!\n";
686             }
687 29 50 50 13   896 binmode $fh, $binmode if length($binmode // '');
  13         90  
  13         32  
  13         110  
688             }
689              
690 29     29   44 return sub ($cmd, @stuff) {
  29         52  
  29         50  
  29         54  
691 29         48 print {$fh} @stuff;
  29         1867  
692 29         212 return $cmd;
693             }
694 29         132077 }
695              
696 29     29   52 sub printout ($self, @stuff) {
  29         93  
  29         69  
  29         53  
697 29         73 my $pof = $self->_rw;
698 29 50       208 $self->_rw($pof = $self->_build_printout_facility) unless $pof;
699 29         110 $pof->($self, @stuff);
700             }
701              
702 7     7   15 sub execute ($self) {
  7         17  
  7         15  
703 7         24 my $target = $self->target;
704 7   33     25 my $name = $target->call_name // $target->name;
705 7 100       29 if (defined(my $commands = $self->list_commands_for($target))) {
706 6         44 $self->printout("sub-commands for $name\n", $commands, "\n");
707             }
708             else {
709 1         6 $self->printout("no sub-commands for $name\n");
710             }
711             } ## end sub execute ($self)
712              
713             package App::Easer::V2::Command::Help;
714             push our @ISA, 'App::Easer::V2::Command::Commands';
715 55     55   223 sub aliases { 'help' }
716 44     44   100 sub allow_residual_options { 0 }
717 1     1   5 sub description { 'Print help for (sub)command' }
718 19     19   60 sub help { 'print a help command' }
719 0     0   0 sub name { 'help' }
720              
721 34     34   54 sub __commandline_help ($getopt) {
  34         56  
  34         49  
722 34         46 my @retval;
723              
724 34         64 my ($mode, $type, $desttype, $min, $max, $default);
725 34 100       291 if (substr($getopt, -1, 1) eq '!') {
    100          
    100          
    100          
    100          
726 15         29 $type = 'bool-negatable';
727 15         50 substr $getopt, -1, 1, '';
728 15         67 push @retval, 'boolean (can be negated)';
729             }
730             elsif ($getopt =~ s<:\+ ([@%])? \z><>mxs) {
731 1         2 $mode = 'optional';
732 1         3 $type = 'i';
733 1         1 $default = 'increment';
734 1         3 $desttype = $1;
735 1         2 my $line = "integer, value is optional, defaults to incrementing current value";
736 1 50 33     4 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
737 1         3 push @retval, $line;
738             } ## end elsif ($getopt =~ s<:+ ([@%])? \z><>mxs)
739             elsif (substr($getopt, -1, 1) eq '+') {
740 1         3 $mode = 'increment';
741 1         2 substr $getopt, -1, 1, '';
742 1         3 push @retval,
743             'incremental integer (adds 1 every time it is provided)';
744             } ## end elsif (substr($getopt, -1...))
745             elsif (
746             $getopt =~ s<(
747             [:=]) # 1 mode
748             ([siof]) # 2 type
749             ([@%])? # 3 desttype
750             (?:
751             \{
752             (\d*)? # 4 min
753             ,?
754             (\d*)? # 5 max
755             \}
756             )? \z><>mxs
757             )
758             {
759 15 100       60 $mode = $1 eq '=' ? 'required' : 'optional';
760 15         39 $type = $2;
761 15         31 $desttype = $3;
762 15         30 $min = $4;
763 15         26 $max = $5;
764 15 50       40 if (defined $min) {
765 0 0       0 $mode = $min ? 'optional' : 'required';
766             }
767             $type = {
768             s => 'string',
769             i => 'integer',
770             o => 'perl-extended-integer',
771             f => 'float',
772 15         84 }->{$type};
773 15         58 my $line = "$type, value is $mode";
774 15 50 33     57 $line .= ", at least $min times" if defined($min) && $min > 1;
775 15 50 33     66 $line .= ", no more than $max times"
776             if defined($max) && length($max);
777 15 100 66     67 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
778 15         32 push @retval, $line;
779             } ## end elsif ($getopt =~ s<( ) )
780             elsif ($getopt =~ s<: (\d+) ([@%])? \z><>mxs) {
781 1         11 $mode = 'optional';
782 1         3 $type = 'i';
783 1         3 $default = $1;
784 1         2 $desttype = $2;
785 1         3 my $line = "integer, value is optional, defaults to $default";
786 1 50 33     4 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
787 1         3 push @retval, $line;
788             } ## end elsif ($getopt =~ s<: (\d+) ([@%])? \z><>mxs)
789             else { # boolean, non-negatable
790 1         3 $type = 'bool';
791 1         3 push @retval, 'boolean';
792             }
793              
794 34         121 my @alternatives = split /\|/, $getopt;
795 34 100 100     283 if ($type eq 'bool-negatable') {
    100          
    100          
796             push @retval, map {
797 15 100       68 if (length($_) == 1) { "-$_" }
  29         88  
  13         45  
798 16         84 else { "--$_ | --no-$_" }
799             } @alternatives;
800             } ## end if ($type eq 'bool')
801             elsif ($type eq 'bool' || $mode eq 'increment') {
802             push @retval, map {
803 2 100       7 if (length($_) == 1) { "-$_" }
  3         7  
  1         3  
804 2         6 else { "--$_" }
805             } @alternatives;
806             } ## end if ($type eq 'bool')
807             elsif ($mode eq 'optional') {
808             push @retval, map {
809 3 50       5 if (length($_) == 1) { "-$_ []" }
  3         7  
  0         0  
810 3         10 else { "--$_ []" }
811             } @alternatives;
812             } ## end elsif ($mode eq 'optional')
813             else {
814             push @retval, map {
815 14 100       52 if (length($_) == 1) { "-$_ " }
  27         59  
  13         48  
816 14         46 else { "--$_ " }
817             } @alternatives;
818             } ## end else [ if ($type eq 'bool') ]
819              
820 34         147 return @retval;
821             } ## end sub __commandline_help ($getopt)
822              
823 22     22   75 sub execute ($self) {
  22         48  
  22         38  
824 22         162 $self->printout($self->collect_help_for($self->target));
825 22         718 return 0;
826             }
827              
828 23     23   74 sub collect_help_for ($self, $target = undef) {
  23         41  
  23         46  
  23         77  
829 23   33     61 $target //= $self->target;
830 23         40 my @stuff;
831              
832 23   100     74 push @stuff, ($target->help // 'no concise help yet'), "\n\n";
833              
834 23 100       102 if (defined(my $description = $target->description)) {
835 21         151 $description =~ s{\A\s+|\s+\z}{}gmxs; # trim
836 21         94 $description =~ s{^}{ }gmxs; # add some indentation
837 21         107 push @stuff, "Description:\n$description\n\n";
838             }
839              
840             # Print this only for sub-commands, not for the root
841 23 100       86 push @stuff, sprintf "Can be called as: %s\n\n", join ', ',
842             $target->aliases
843             if $target->parent;
844              
845 23 100       98 if (my @options = $target->options) {
846 17         53 push @stuff, "Options:\n";
847 17         33 my $n = 0; # count the option
848 17         72 for my $opt (@options) {
849 34 100       121 push @stuff, "\n" if $n++; # from second line on
850              
851             push @stuff, sprintf "%15s: %s\n", $target->name_for_option($opt),
852 34   100     100 $opt->{help} // '';
853              
854 34 50       108 if (exists $opt->{getopt}) {
855 34         102 my @lines = __commandline_help($opt->{getopt});
856 34         160 push @stuff, sprintf "%15s command-line: %s\n", '',
857             shift(@lines);
858             push @stuff,
859 34         70 map { sprintf "%15s %s\n", '', $_ } @lines;
  62         214  
860             } ## end if (exists $opt->{getopt...})
861              
862 34 100       160 if (defined(my $env = $self->environment_variable_name($opt))) {
863 14         58 push @stuff, sprintf "%15s environment: %s\n", '', $env;
864             }
865              
866             push @stuff, sprintf "%15s default: %s\n", '',
867             $opt->{default} // '*undef*'
868 34 100 50     161 if exists $opt->{default};
869             } ## end for my $opt (@options)
870              
871 17         48 push @stuff, "\n";
872             } ## end if (my @options = $target...)
873             else {
874 6         23 push @stuff, "This command has no option\n";
875             }
876              
877 23 100       115 if (defined(my $commands = $self->list_commands_for($target))) {
878 12         39 push @stuff, "Sub-commands:\n", $commands, "\n";
879             }
880             else {
881 11         28 push @stuff, "No sub-commands\n";
882             }
883              
884 23         250 return join '', @stuff;
885             } ## end sub execute ($self)
886              
887             package App::Easer::V2::Command::Tree;
888             push our @ISA, 'App::Easer::V2::Command::Commands';
889 26     26   75 sub aliases { 'tree' }
890 1     1   4 sub description { 'Print tree of supported sub-commands' }
891 19     19   44 sub help { 'print sub-commands in a tree' }
892 0     0   0 sub name { 'tree' }
893              
894             sub options {
895             return (
896             {
897 1     1   9 getopt => 'include_auto|include-auto|I!',
898             default => 0,
899             environment => 1,
900             },
901             );
902             } ## end sub options
903              
904 0     0   0 sub list_commands_for ($self, $target) {
  0         0  
  0         0  
  0         0  
905 0 0       0 my $exclude_auto = $self->config('include_auto') ? 0 : 1;
906 0         0 my @lines;
907 0         0 for my $command ($target->inflate_children($target->list_children)) {
908 0 0       0 my ($name) = $command->aliases or next;
909             next
910 0 0 0     0 if $name =~ m{\A(?: help | commands | tree)\z}mxs && $exclude_auto;
911 0   0     0 my $help = $command->help // '(**missing help**)';
912 0         0 push @lines, sprintf '- %s (%s)', $name, $help;
913 0 0       0 if (defined(my $subtree = $self->list_commands_for($command))) {
914 0         0 push @lines, $subtree =~ s{^}{ }rgmxs;
915             }
916             } ## end for my $command ($target...)
917 0 0       0 return unless @lines;
918 0         0 return join "\n", @lines;
919             } ## end sub list_commands_for
920              
921             1;