File Coverage

blib/lib/App/Easer/V1.pm
Criterion Covered Total %
statement 561 609 92.1
branch 164 228 71.9
condition 94 147 63.9
subroutine 57 59 96.6
pod 47 47 100.0
total 923 1090 84.6


line stmt bran cond sub pod time code
1             package App::Easer::V1;
2 22     22   68686 use v5.24;
  22         92  
3 22     22   123 use warnings;
  22         39  
  22         648  
4 22     22   542 use experimental qw< signatures >;
  22         3445  
  22         122  
5 22     22   2599 no warnings qw< experimental::signatures >;
  22         43  
  22         1657  
6             { our $VERSION = '2.006' }
7              
8 22     22   2310 use parent 'Exporter';
  22         1533  
  22         121  
9             our @EXPORT_OK = qw< d run appeaser_api >;
10              
11 103     103 1 183 sub add_auto_commands ($application) {
  103         151  
  103         133  
12 103         193 my $commands = $application->{commands};
13             $commands->{help} //= {
14 103   100     473 name => 'help',
15             supports => ['help'],
16             help => 'print a help message',
17             description => 'print help for (sub)command',
18             'allow-residual-options' => 0,
19             leaf => 1,
20             execute => \&stock_help,
21             };
22             $commands->{commands} //= {
23 103   100     412 name => 'commands',
24             supports => ['commands'],
25             help => 'list sub-commands',
26             description => 'Print list of supported sub-commands',
27             'allow-residual-options' => 0,
28             leaf => 1,
29             execute => \&stock_commands,
30             };
31 103         183 return $application;
32             } ## end sub add_auto_commands ($application)
33              
34 3     3 1 3797 sub appeaser_api { return 'V1' }
35              
36 183     183 1 244 sub collect ($self, $spec, $args) {
  183         331  
  183         235  
  183         245  
  183         230  
37 183         237 my @sequence;
38 183         279 my $config = {};
39 183         253 my @residual_args;
40              
41 183         355 my $merger = merger($self, $spec);
42              
43 183         475 for my $source_spec (sources($self, $spec, $args)) {
44 734 50       2020 my ($src, $src_cnf) =
45             'ARRAY' eq ref $source_spec
46             ? $source_spec->@*
47             : ($source_spec, {});
48 734         1438 $src = $self->{factory}->($src, 'collect'); # "resolve"
49 734         3696 $src_cnf = {$spec->%*, $src_cnf->%*, config => $config};
50 734         1768 my ($slice, $residual_args) = $src->($self, $src_cnf, $args);
51 734 100       2111 push @residual_args, $residual_args->@* if defined $residual_args;
52 734         1164 push @sequence, $slice;
53 734         1354 $config = $merger->(@sequence);
54             } ## end for my $source_spec (sources...)
55              
56 183         669 return ($config, \@residual_args);
57             } ## end sub collect
58              
59 184     184 1 249 sub collect_options ($self, $spec, $args) {
  184         242  
  184         285  
  184         236  
  184         261  
60 184         285 my $factory = $self->{factory};
61             my $collect = $spec->{collect}
62 184   66     888 // $self->{application}{configuration}{collect} // \&collect;
      100        
63 184         355 my $collector = $factory->($collect, 'collect'); # "resolve"
64 184         450 (my $config, $args) = $collector->($self, $spec, $args);
65 184         415 push $self->{configs}->@*, $config;
66 184         356 return $args;
67             } ## end sub collect_options
68              
69 25     25 1 42 sub commandline_help ($getopt) {
  25         46  
  25         35  
70 25         38 my @retval;
71              
72 25         51 my ($mode, $type, $desttype, $min, $max, $default);
73 25 100       241 if (substr($getopt, -1, 1) eq '!') {
    50          
    50          
    0          
    0          
74 10         24 $type = 'bool';
75 10         25 substr $getopt, -1, 1, '';
76 10         26 push @retval, 'boolean option';
77             }
78             elsif (substr($getopt, -1, 1) eq '+') {
79 0         0 $mode = 'increment';
80 0         0 substr $getopt, -1, 1, '';
81 0         0 push @retval,
82             'incremental option (adds 1 every time it is provided)';
83             } ## end elsif (substr($getopt, -1...))
84             elsif (
85             $getopt =~ s<(
86             [:=]) # 1 mode
87             ([siof]) # 2 type
88             ([@%])? # 3 desttype
89             (?:
90             \{
91             (\d*)? # 4 min
92             ,?
93             (\d*)? # 5 max
94             \}
95             )? \z><>mxs
96             )
97             {
98 15 50       80 $mode = $1 eq '=' ? 'mandatory' : 'optional';
99 15         64 $type = $2;
100 15         44 $desttype = $3;
101 15         28 $min = $4;
102 15         28 $max = $5;
103 15 50       73 if (defined $min) {
104 0 0       0 $mode = $min ? 'optional' : 'required';
105             }
106             $type = {
107             s => 'string',
108             i => 'integer',
109             o => 'perl-extended-integer',
110             f => 'float',
111 15         262 }->{$type};
112 15         73 my $line = "$mode $type option";
113 15 50 33     96 $line .= ", at least $min times" if defined($min) && $min > 1;
114 15 50 33     96 $line .= ", no more than $max times"
115             if defined($max) && length($max);
116 15 50 33     222 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
117 15         56 push @retval, $line;
118             } ## end elsif ($getopt =~ s<( )? \z><>mxs)
119             elsif ($getopt =~ s<: (\d+) ([@%])? \z><>mxs) {
120 0         0 $mode = 'optional';
121 0         0 $type = 'i';
122 0         0 $default = $1;
123 0         0 $desttype = $2;
124 0         0 my $line = "optional integer, defaults to $default";
125 0 0 0     0 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
126 0         0 push @retval, $line;
127             } ## end elsif ($getopt =~ s<: (\d+) ([@%])? \z><>mxs)
128             elsif ($getopt =~ s<:+ ([@%])? \z><>mxs) {
129 0         0 $mode = 'optional';
130 0         0 $type = 'i';
131 0         0 $default = 'increment';
132 0         0 $desttype = $1;
133 0         0 my $line = "optional integer, current value incremented if omitted";
134 0 0 0     0 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
135 0         0 push @retval, $line;
136             } ## end elsif ($getopt =~ s<:+ ([@%])? \z><>mxs)
137              
138 25         91 my @alternatives = split /\|/, $getopt;
139 25 100       151 if ($type eq 'bool') {
    50          
140             push @retval, map {
141 10 100       33 if (length($_) == 1) { "-$_" }
  18         65  
  8         34  
142 10         56 else { "--$_ | --no-$_" }
143             } @alternatives;
144             } ## end if ($type eq 'bool')
145             elsif ($mode eq 'optional') {
146             push @retval, map {
147 0 0       0 if (length($_) == 1) { "-$_ []" }
  0         0  
  0         0  
148 0         0 else { "--$_ []" }
149             } @alternatives;
150             } ## end elsif ($mode eq 'optional')
151             else {
152             push @retval, map {
153 15 100       43 if (length($_) == 1) { "-$_ " }
  30         130  
  15         61  
154 15         123 else { "--$_ " }
155             } @alternatives;
156             } ## end else [ if ($type eq 'bool') ]
157              
158 25         107 return @retval;
159             } ## end sub commandline_help ($getopt)
160              
161 184     184 1 235 sub commit_configuration ($self, $spec, $args) {
  184         253  
  184         235  
  184         226  
  184         230  
162 184   50     397 my $commit = $spec->{commit} // return;
163 0         0 $self->{factory}->($commit, 'commit')->($self, $spec, $args);
164             }
165              
166 0     0 1 0 sub d (@stuff) {
  0         0  
  0         0  
167 22     22   33926 no warnings;
  22         67  
  22         155219  
168 0         0 require Data::Dumper;
169 0         0 local $Data::Dumper::Indent = 1;
170 0 0       0 warn Data::Dumper::Dumper(@stuff % 2 ? \@stuff : {@stuff});
171             } ## end sub d (@stuff)
172              
173 183     183 1 261 sub default_getopt_config ($self, $spec) {
  183         240  
  183         229  
  183         229  
174 183         364 my @r = qw< gnu_getopt >;
175 183 100       377 push @r, qw< require_order pass_through >
176             if has_children($self, $spec);
177 183 50       415 push @r, qw< pass_through > if $spec->{'allow-residual-options'};
178 183         496 return \@r;
179             }
180              
181 93     93 1 155 sub execute ($self, $args) {
  93         142  
  93         117  
  93         130  
182 93         175 my $command = $self->{trail}[-1][0];
183             my $executable = fetch_spec_for($self, $command)->{execute}
184 93 50       175 or die "no executable for '$command'\n";
185 93         202 $executable = $self->{factory}->($executable, 'execute'); # "resolve"
186 93   50     310 my $config = $self->{configs}[-1] // {};
187 93         295 return $executable->($self, $config, $args);
188             } ## end sub execute
189              
190 37     37 1 58 sub fetch_subcommand_default ($self, $spec) {
  37         59  
  37         51  
  37         49  
191 37         75 my $acfg = $self->{application}{configuration};
192             my $child = exists($spec->{'default-child'}) ? $spec->{'default-child'}
193 37 0       96 : exists($acfg->{'default-child'}) ? $acfg->{'default-child'}
    50          
194             : get_child($self, $spec, 'help'); # help is last resort
195 37 100 100     233 return ($child, $child) if defined $child && length $child;
196 23         112 return;
197             }
198              
199 184     184 1 248 sub fetch_subcommand ($self, $spec, $args) {
  184         228  
  184         227  
  184         235  
  184         252  
200 184 100       371 my ($subc, $alias) = fetch_subcommand_wh($self, $spec, $args)
201             or return;
202 85         168 my $r = ref $subc;
203 85 100       191 if ($r eq 'HASH') {
204             $subc = $spec->{children}[$subc->{index}]
205 3 100 66     15 if scalar(keys $subc->%*) == 1 && defined $subc->{index};
206 3         6 $r = ref $subc;
207 3 100       13 return ($subc, $subc->{supports}[0]) if $r eq 'HASH';
208 1         2 $alias = $subc;
209             }
210 83 50       194 die "invalid sub-command (ref to $r)" if $r;
211 83         282 return ($subc, $alias);
212             }
213              
214 184     184 1 230 sub fetch_subcommand_wh ($self, $spec, $args) {
  184         229  
  184         235  
  184         225  
  184         219  
215             # if there's a dispatch, use that to figure out where to go next
216             # **this** might even overcome having children at all!
217 184         358 for my $cfg ($spec, $self->{application}{configuration}) {
218 366 100       757 next unless exists $cfg->{dispatch};
219 2         5 my $sub = $self->{factory}->($cfg->{dispatch}, 'dispatch');
220 2 50       8 defined(my $child = $sub->($self, $spec, $args)) or return;
221 2         15 return ($child, $child);
222             }
223              
224             # regular course here, no point in going forth without children
225 182 100       357 return unless has_children($self, $spec);
226              
227             # use defaults if there's no argument to investigate
228 112 100       310 return fetch_subcommand_default($self, $spec) unless $args->@*;
229              
230             # try to get a child from the first argument
231 77 100       227 if (my $child = get_child($self, $spec, $args->[0])) {
232 65         248 return ($child, shift $args->@*); # consumed arg name
233             }
234              
235             # the first argument didn't help, but we might want to fallback
236 12         32 for my $cfg ($spec, $self->{application}{configuration}) {
237 18 100       89 if (exists $cfg->{fallback}) { # executable
238 2 50       6 defined(my $fb = $cfg->{fallback}) or return;
239 2         5 my $sub = $self->{factory}->($fb, 'fallback'); # "resolve"
240 2 50       7 defined(my $child = $sub->($self, $spec, $args)) or return;
241 2         15 return ($child, $child);
242             }
243 16 100       39 if (exists $spec->{'fallback-to'}) {
244 2 50       11 defined(my $fbto = $spec->{'fallback-to'}) or return;
245 2         9 return ($fbto, $fbto);
246             }
247             return fetch_subcommand_default($self, $spec)
248 14 100       58 if $cfg->{'fallback-to-default'};
249             }
250              
251             # no fallback at this point... it's an error, build a message and die!
252 6         26 my @names = map { $_->[1] } $self->{trail}->@*;
  6         21  
253 6         14 shift @names; # remove first one
254 6         22 my $path = join '/', @names, $args->[0]; # $args->[0] was the candidate
255 6         85 die "cannot find sub-command '$path'\n";
256             } ## end sub fetch_subcommand_wh
257              
258 104     104 1 271 sub generate_factory ($c) {
  104         158  
  104         151  
259 104         193 my $w = \&stock_factory; # default factory
260 104 50       283 $w = stock_factory($c->{create}, 'factory', $c) if defined $c->{create};
261 104     2166   1102 return sub ($e, $d = '') { $w->($e, $d, $c) };
  2166         2465  
  2166         3491  
  2166         9779  
  2166         2873  
  2166         2842  
262             }
263              
264 88     88 1 125 sub get_child ($self, $spec, $name) {
  88         125  
  88         115  
  88         126  
  88         119  
265 88         181 for my $child (get_children($self, $spec)) {
266 157         297 my $command = fetch_spec_for($self, $child);
267             next
268 199         585 unless grep { $_ eq $name }
269 157 100 100     489 ($command->{supports} //= [$child])->@*;
270 76         284 return $child;
271             } ## end for my $child (get_children...)
272 12         44 return;
273             } ## end sub get_child
274              
275 10     10 1 20 sub stock_ChildrenByPrefix ($self, $spec, @prefixes) {
  10         13  
  10         14  
  10         31  
  10         17  
276 10         48 require File::Spec;
277             my @expanded_inc = map {
278 10         21 my ($v, $dirs) = File::Spec->splitpath($_, 'no-file');
  90         330  
279 90         541 [$v, File::Spec->splitdir($dirs)];
280             } @INC;
281 10         17 my %seen;
282             return map {
283 10         15 my @parts = split m{::}mxs, $_ . 'x';
  10         31  
284 10         27 substr(my $bprefix = pop @parts, -1, 1, '');
285             map {
286 10         17 my ($v, @dirs) = $_->@*;
  90         289  
287 90         607 my $dirs = File::Spec->catdir(@dirs, @parts);
288 90 100       1891 if (opendir my $dh, File::Spec->catpath($v, $dirs, '')) {
289 18         179 grep { ! $seen{$_}++ }
290             map {
291 18         40 substr(my $lastpart = $_, -3, 3, '');
292 18         49 join '::', @parts, $lastpart;
293             } grep {
294 10         261 my $path = File::Spec->catpath($v, $dirs, $_);
  46         309  
295 46 100 66     1116 (-e $path && ! -d $path)
      66        
296             && substr($_, 0, length($bprefix)) eq $bprefix
297             && substr($_, -3, 3) eq '.pm'
298             } readdir $dh;
299             }
300 80         455 else { () }
301             } @expanded_inc;
302             } @prefixes;
303             }
304              
305 126     126 1 156 sub expand_children ($self, $spec, $child_spec) {
  126         155  
  126         159  
  126         158  
  126         149  
306 126 100       405 return $child_spec unless ref($child_spec) eq 'ARRAY';
307 10         21 my ($exe, @args) = $child_spec->@*;
308 10         21 return $self->{factory}->($exe, 'children')->($self, $spec, @args);
309             }
310              
311 482     482 1 587 sub get_children ($self, $spec, $expand = 1) {
  482         604  
  482         581  
  482         683  
  482         544  
312 482 100       1188 return if $spec->{leaf};
313 411 100 100     1290 return if exists($spec->{children}) && !$spec->{children};
314 388   100     1223 my @children = ($spec->{children} // [])->@*;
315              
316             # set auto-leaves as 1 by default, new in 0.007002
317             $self->{application}{configuration}{'auto-leaves'} = 1
318 388 100       947 unless exists $self->{application}{configuration}{'auto-leaves'};
319              
320             return
321 388 100 100     1391 if $self->{application}{configuration}{'auto-leaves'}
322             && @children == 0; # no auto-children for leaves under auto-leaves
323              
324             # skip expansion if $expand is false (default is expand)
325 333 100       766 @children = map { expand_children($self, $spec, $_) } @children
  126         231  
326             if $expand;
327              
328             my @auto =
329             exists $self->{application}{configuration}{'auto-children'}
330 333 50 0     842 ? (($self->{application}{configuration}{'auto-children'} // [])->@*)
331             : (qw< help commands >);
332 333 50       649 if (exists $spec->{'no-auto'}) {
333 0 0       0 if (ref $spec->{'no-auto'}) {
    0          
334 0         0 my %no = map { $_ => 1 } $spec->{'no-auto'}->@*;
  0         0  
335 0         0 @auto = grep { !$no{$_} } @auto;
  0         0  
336             }
337             elsif ($spec->{'no-auto'} eq '*') {
338 0         0 @auto = ();
339             }
340             else {
341 0         0 die "invalid no-auto, array or '*' are allowed\n";
342             }
343             } ## end if (exists $spec->{'no-auto'...})
344 333         1238 return (@children, @auto);
345             } ## end sub get_children
346              
347             # traverse a whole @$list of sub-commands from $start. This is used to
348             # list "commands" at a certain sub-level or show help
349 27     27 1 51 sub get_descendant ($self, $start, $list) {
  27         40  
  27         51  
  27         70  
  27         42  
350 27         42 my $target = $start;
351 27         43 my $path;
352 27         63 for my $desc ($list->@*) {
353 11 50       35 $path = defined($path) ? "$path/$desc" : $desc;
354 11 50       26 my $command = fetch_spec_for($self, $target)
355             or die "cannot find sub-command '$path'\n";
356 11 50       26 defined($target = get_child($self, $command, $desc))
357             or die "cannot find sub-command '$path'\n";
358             } ## end for my $desc ($list->@*)
359              
360             # check that this last is associated to a real command
361 27 50       51 return $target if fetch_spec_for($self, $target);
362 0         0 die "cannot find sub-command '$path'\n";
363             } ## end sub get_descendant
364              
365 365 100   365 1 473 sub has_children ($self, $spec) { get_children($self, $spec, 0) ? 1 : 0 }
  365         513  
  365         451  
  365         462  
  365         709  
366              
367             sub hash_merge {
368 737     737 1 1052 my (%retval, %is_overridable);
369 737         1271 for my $href (@_) {
370 1850         3466 for my $src_key (keys $href->%*) {
371 638         930 my $dst_key = $src_key;
372 638         763 my $this_overridable;
373 638 100       1913 if ($dst_key =~ m{\A //= (.*) \z}mxs) { # overridable
374 354         792 $dst_key = $1;
375 354 50       874 $is_overridable{$dst_key} = 1 unless exists $retval{$dst_key};
376 354         497 $this_overridable = 1;
377             }
378             $retval{$dst_key} = $href->{$src_key}
379 638 100 100     2118 if $is_overridable{$dst_key} || ! exists($retval{$dst_key});
380 638 100       1626 $is_overridable{$dst_key} = 0 unless $this_overridable;
381             }
382             }
383 737         2515 return \%retval;
384             # was a simple: return {map { $_->%* } reverse @_};
385             }
386              
387 19     19 1 70 sub list_commands ($self, $children) {
  19         38  
  19         49  
  19         25  
388 19         38 my $retval = '';
389 19     10   472 open my $fh, '>', \$retval;
  10         76  
  10         16  
  10         85  
390 19         6953 for my $child ($children->@*) {
391 49         106 my $command = fetch_spec_for($self, $child);
392 49         106 my $help = $command->{help};
393 49   50     145 my @aliases = ($command->{supports} // [$child])->@*;
394 49 50       105 next unless @aliases;
395 49         77 printf {$fh} "%15s: %s\n", shift(@aliases), $help;
  49         253  
396 49 100       164 printf {$fh} "%15s (also as: %s)\n", '', join ', ', @aliases
  2         13  
397             if @aliases;
398             } ## end for my $child ($children...)
399 19         84 close $fh;
400 19         645 return $retval;
401             } ## end sub list_commands
402              
403 109     109 1 376 sub load_application ($application) {
  109         174  
  109         182  
404 109 100       503 return $application if 'HASH' eq ref $application;
405              
406 5         7 my $text;
407 5 100       14 if ('SCALAR' eq ref $application) {
408 2         4 $text = $$application;
409             }
410             else {
411             my $fh =
412             'GLOB' eq ref $application
413             ? $application
414 3 100       12 : do {
415 2 50       88 open my $fh, '<:encoding(UTF-8)', $application
416             or die "cannot open '$application'\n";
417 2         11778 $fh;
418             };
419 3         13 local $/; # slurp mode
420 3         122 $text = <$fh>;
421 3         89 close $fh;
422             } ## end else [ if ('SCALAR' eq ref $application)]
423              
424             return eval {
425 5         760 require JSON::PP;
426 5         15019 JSON::PP::decode_json($text);
427 5   66     12 } // eval { eval $text; } // die "cannot load application\n";
  3   50     1987  
428             } ## end sub load_application ($application)
429              
430 186     186 1 543 sub merger ($self, $spec = {}) {
  186         250  
  186         259  
  186         247  
431             my $merger = $spec->{merge}
432 186   33     1290 // $self->{application}{configuration}{merge} // \&hash_merge;
      50        
433 186         381 return $self->{factory}->($merger, 'merge'); # "resolve"
434             }
435              
436 204     204 1 276 sub env_namer ($self, $cspec) {
  204         271  
  204         255  
  204         250  
437             my $namenv = $cspec->{namenv}
438 204   33     1089 // $self->{application}{configuration}{namenv} // \&stock_NamEnv;
      50        
439 204         428 $namenv = $self->{factory}->($namenv, 'namenv'); # "resolve"
440 204     263   1013 return sub ($ospec) { $namenv->($self, $cspec, $ospec) };
  263         354  
  263         512  
  263         331  
  263         343  
441             } ## end sub name_for_option ($o)
442              
443 182     182 1 240 sub name_for_option ($o) {
  182         411  
  182         484  
444 182 50       440 return $o->{name} if defined $o->{name};
445 182 50 33     2208 return $1 if defined $o->{getopt} && $o->{getopt} =~ m{\A(\w+)}mxs;
446             return lc $o->{environment}
447 0 0 0     0 if defined $o->{environment} && $o->{environment} ne '1';
448 0         0 return '~~~';
449             } ## end sub name_for_option ($o)
450              
451 184     184 1 233 sub params_validate ($self, $spec, $args) {
  184         244  
  184         215  
  184         241  
  184         226  
452             my $validator = $spec->{validate}
453 184   33     768 // $self->{application}{configuration}{validate} // return;
      50        
454 0         0 require Params::Validate;
455 0         0 Params::Validate::validate($self->{configs}[-1]->%*, $validator);
456             } ## end sub params_validate
457              
458 8     8 1 44 sub print_commands ($self, $target) {
  8         15  
  8         15  
  8         12  
459 8         25 my $command = fetch_spec_for($self, $target);
460             my $fh =
461 8 50       56 $self->{application}{configuration}{'help-on-stderr'}
462             ? \*STDERR
463             : \*STDOUT;
464 8 100       36 if (my @children = get_children($self, $command)) {
465 7         20 print {$fh} list_commands($self, \@children);
  7         29  
466             }
467             else {
468 1         2 print {$fh} "no sub-commands\n";
  1         60  
469             }
470             }
471              
472 21     21 1 67 sub print_help ($self, $target) {
  21         28  
  21         34  
  21         31  
473 21         48 my $command = fetch_spec_for($self, $target);
474 21         82 my $enamr = env_namer($self, $command);
475             my $fh =
476 21 50       106 $self->{application}{configuration}{'help-on-stderr'}
477             ? \*STDERR
478             : \*STDOUT;
479              
480 21         44 print {$fh} $command->{help}, "\n\n";
  21         953  
481              
482 21 100       162 if (defined(my $description = $command->{description})) {
483 20         173 $description =~ s{\A\s+|\s+\z}{}gmxs; # trim
484 20         126 $description =~ s{^}{ }gmxs; # add some indentation
485 20         38 print {$fh} "Description:\n$description\n\n";
  20         312  
486             }
487              
488 13         196 printf {$fh} "Can be called as: %s\n\n", join ', ',
489             $command->{supports}->@*
490 21 100       105 if $command->{supports};
491              
492 21   100     119 my $options = $command->{options} // [];
493 21 100       78 if ($options->@*) {
494 15         50 print {$fh} "Options:\n";
  15         179  
495 15         57 my $n = 0; # count the option
496 15         41 for my $option ($options->@*) {
497 25 100       80 print {$fh} "\n" if $n++;
  10         108  
498              
499 25         82 printf {$fh} "%15s: %s\n", name_for_option($option),
500 25   50     58 $option->{help} // '';
501              
502 25 50       140 if (exists $option->{getopt}) {
503 25         105 my @lines = commandline_help($option->{getopt});
504 25         54 printf {$fh} "%15s command-line: %s\n", '', shift(@lines);
  25         414  
505 25         176 printf {$fh} "%15s %s\n", '', $_ for @lines;
  48         651  
506             }
507              
508 25 100       110 if (defined(my $env_name = $enamr->($option))) {
509 16         32 printf {$fh} "%15s environment : %s\n", '', $env_name;
  16         240  
510             }
511              
512 8         194 printf {$fh} "%15s default : %s\n", '',
513             $option->{default} // '*undef*'
514 25 100 50     124 if exists $option->{default};
515             } ## end for my $option ($options...)
516 15         45 print {$fh} "\n";
  15         209  
517             } ## end if ($options->@*)
518             else {
519 6         12 print {$fh} "This command has no options.\n\n";
  6         66  
520             }
521              
522 21 100       103 if (my @children = get_children($self, $command)) {
523 12         27 print {$fh} "Sub commands:\n", list_commands($self, \@children),
  12         58  
524             "\n";
525             }
526             else {
527 9         18 print {$fh} "no sub-commands\n\n";
  9         141  
528             }
529             }
530              
531 403     403 1 498 sub stock_SpecFromHash ($s, $cmd) {
  403         500  
  403         529  
  403         465  
532 403 100       768 return $cmd if ref($cmd) eq 'HASH';
533 390   100     1481 return $s->{application}{commands}{$cmd} // undef;
534             }
535              
536 151     151 1 189 sub stock_SpecFromHashOrModule ($s, $cmd) {
  151         207  
  151         184  
  151         173  
537 151 50       259 return $cmd if ref($cmd) eq 'HASH';
538             return $s->{application}{commands}{$cmd}
539 151   66     497 //= $s->{factory}->($cmd, 'spec')->();
540             }
541              
542 554     554 1 695 sub fetch_spec_for ($self, $command) {
  554         706  
  554         732  
  554         721  
543             my $fetcher = $self->{application}{configuration}{specfetch}
544 554   100     1621 // \&stock_SpecFromHash;
545 554         1151 return $self->{factory}->($fetcher, 'specfetch')->($self, $command);
546             }
547              
548 103     103 1 450557 sub run ($application, $args) {
  103         201  
  103         146  
  103         151  
549 103         282 $application = add_auto_commands(load_application($application));
550             my $self = {
551             application => $application,
552             configs => [],
553             factory => generate_factory($application->{factory} // {}),
554             helpers => {
555             'print-commands' => \&print_commands,
556             'print-help' => \&print_help,
557             },
558 103   50     631 trail => [['MAIN', $application->{commands}{MAIN}{name}]],
559             };
560              
561 103         253 while ('necessary') {
562 188         432 my $command = $self->{trail}[-1][0];
563 188 100       362 my $spec = fetch_spec_for($self, $command)
564             or die "no definition for '$command'\n";
565              
566 184         451 $args = collect_options($self, $spec, $args);
567 184         495 validate_configuration($self, $spec, $args);
568 184         533 commit_configuration($self, $spec, $args);
569              
570 184 100       370 my ($subc, $alias) = fetch_subcommand($self, $spec, $args) or last;
571 85         314 push $self->{trail}->@*, [$subc, $alias];
572             } ## end while ('necessary')
573              
574 93   50     239 return execute($self, $args) // 0;
575             } ## end sub run
576              
577 2     2 1 4 sub slurp ($file, $mode = '<:encoding(UTF-8)') {
  2         4  
  2         4  
  2         3  
578 2 50       68 open my $fh, $mode, $file or die "open('$file'): $!\n";
579 2         11855 local $/;
580 2         77 return <$fh>;
581             }
582              
583 183     183 1 258 sub sources ($self, $spec, $args) {
  183         248  
  183         238  
  183         233  
  183         224  
584             my $s = $spec->{sources}
585             // $self->{application}{configuration}{sources}
586 183   100     862 // \&stock_DefaultSources;
      100        
587 183 100       528 $s = $self->{factory}->($s, 'sources')->() if 'ARRAY' ne ref $s;
588 183         627 return $s->@*;
589             } ## end sub sources
590              
591 183     183 1 271 sub stock_CmdLine ($self, $spec, $args) {
  183         252  
  183         226  
  183         240  
  183         233  
592 183         370 my @args = $args->@*;
593 183   33     623 my $goc = $spec->{'getopt-config'}
594             // default_getopt_config($self, $spec);
595 183         13432 require Getopt::Long;
596 183         174074 Getopt::Long::Configure('default', $goc->@*);
597              
598 183         11251 my %option_for;
599             my @specs = map {
600 238         407 my $go = $_->{getopt};
601             ref($go) eq 'ARRAY'
602 0     0   0 ? ($go->[0] => sub { $go->[1]->(\%option_for, @_) })
603 238 50       661 : $go;
604             }
605 183   100     674 grep { exists $_->{getopt} } ($spec->{options} // [])->@*;
  238         592  
606 183 50       566 Getopt::Long::GetOptionsFromArray(\@args, \%option_for, @specs)
607             or die "bailing out\n";
608              
609             # Check if we want to forbid the residual @args to start with a '-'
610 183         33581 my $strict = !$spec->{'allow-residual-options'};
611 183 50 66     1082 if ($strict && @args && $args[0] =~ m{\A -}mxs) {
      66        
612 0         0 Getopt::Long::Configure('default', 'gnu_getopt');
613 0         0 Getopt::Long::GetOptionsFromArray(\@args, {});
614 0         0 die "bailing out\n";
615             }
616              
617 183         652 return (\%option_for, \@args);
618             } ## end sub stock_CmdLine
619              
620 3     3 1 5 sub stock_JsonFileFromConfig ($self, $spec, $args) {
  3         4  
  3         8  
  3         4  
  3         4  
621 3   50     12 my $key = $spec->{'config-option'} // 'config';
622 3 100       11 return {} if !exists($spec->{config}{$key});
623 1         708 require JSON::PP;
624 1         14791 return JSON::PP::decode_json(slurp($spec->{config}{$key}));
625             } ## end sub stock_JsonFileFromConfig
626              
627 3     3 1 6 sub stock_JsonFiles ($self, $spec, @ignore) {
  3         5  
  3         4  
  3         4  
  3         4  
628             return merger($self, $spec)->(
629             map {
630 1         8 require JSON::PP;
631 1         3 JSON::PP::decode_json(slurp($_));
632             }
633 3   100     18 grep { -e $_ } ($spec->{'config-files'} // [])->@*
  2         56  
634             );
635             } ## end sub stock_JsonFiles
636              
637 181     181 1 244 sub stock_Default ($self, $spec, @ignore) {
  181         237  
  181         231  
  181         323  
  181         232  
638             return {
639 87         225 map { '//=' . name_for_option($_) => $_->{default} }
640 181   100     649 grep { exists $_->{default} } ($spec->{options} // [])->@*
  230         644  
641             };
642             } ## end sub stock_Default
643              
644 183     183 1 261 sub stock_Environment ($self, $spec, @ignore) {
  183         247  
  183         238  
  183         280  
  183         226  
645 183         399 my $enamr = env_namer($self, $spec);
646             return {
647             map {
648 238         437 my $en = $enamr->($_); # name of environment variable
649             defined($en) && exists($ENV{$en})
650 238 100 100     1343 ? (name_for_option($_) => $ENV{$en}) : ();
651 183   100     777 } ($spec->{options} // [])->@*
652             };
653             } ## end sub stock_Environment
654              
655 263     263 1 342 sub stock_NamEnv ($self, $cspec, $ospec) {
  263         340  
  263         324  
  263         311  
  263         319  
656 263         360 my $aek = 'auto-environment';
657             my $autoenv = exists $cspec->{$aek} ? $cspec->{$aek}
658 263 100 100     932 : $self->{application}{configuration}{$aek} // undef;
659             my $env = exists $ospec->{environment} ? $ospec->{environment}
660 263 100       580 : $autoenv ? 1 : undef;
    100          
661 263 100 100     955 return $env unless ($env // '') eq '1';
662 36   50     87 my $appname = $self->{application}{configuration}{name} // '';
663 36         65 my $optname = name_for_option($ospec);
664 36         150 return uc(join '_', $appname, $optname);
665             }
666              
667 181   100 181 1 257 sub stock_Parent ($self, $spec, @ignore) { $self->{configs}[-1] // {} }
  181         260  
  181         238  
  181         259  
  181         218  
  181         708  
668              
669 7     7 1 16 sub stock_commands ($self, $config, $args) {
  7         17  
  7         13  
  7         14  
  7         14  
670 7 50       28 die "this command does not support arguments\n" if $args->@*;
671 7         30 my $target = get_descendant($self, $self->{trail}[-2][0], $args);
672 7         43 print_commands($self, $target);
673 7         116 return 0;
674             } ## end sub stock_commands
675              
676 2166     2166 1 2586 sub stock_factory ($executable, $default_subname = '', $opts = {}) {
  2166         2719  
  2166         2670  
  2166         2588  
  2166         2447  
677 90     90   148 state $factory = sub ($executable, $default_subname) {
  90         138  
  90         157  
  90         146  
678             my @prefixes =
679             !defined $opts->{prefixes} ? ()
680             : 'ARRAY' eq ref $opts->{prefixes} ? ($opts->{prefixes}->@*)
681 90 50       281 : ($opts->{prefixes});
    100          
682 90         833 push @prefixes, {'+' => 'App::Easer::V1#stock_'};
683             SEARCH:
684 90         210 for my $expansion_for (@prefixes) {
685 96         259 for my $p (keys $expansion_for->%*) {
686 111 100       345 next if $p ne substr $executable, 0, length $p;
687 74         184 substr $executable, 0, length $p, $expansion_for->{$p};
688 74         209 last SEARCH;
689             }
690             } ## end SEARCH: for my $expansion_for (...)
691              
692             # if it *still* "starts" with '=', it's "inline" Perl code
693 90 100       413 return eval $executable if $executable =~ s{\A \s* = \s* }{}mxs;
694              
695 89         314 my ($package, $sname) = split m{\#}mxs, $executable;
696 89 100 66     412 $sname = $default_subname unless defined $sname && length $sname;
697              
698             # first try to see if the sub is already available in $package
699 89 100       798 if (my $s = $package->can($sname)) { return $s }
  83         363  
700              
701             # otherwise force loading of $package and retry
702 6         27 (my $path = "$package.pm") =~ s{::}{/}gmxs;
703 6         2083 require $path;
704 4 50       84 if (my $s = $package->can($sname)) { return $s }
  4         24  
705              
706 0         0 die "no '$sname' in '$package'\n";
707 2166         2764 };
708 2166         2537 state $cache = {};
709              
710 2166         2558 my $args;
711 2166 100       4228 ($executable, $args) = ($executable->{executable}, $executable)
712             if 'HASH' eq ref $executable;
713 2166 100 100     5481 $executable = $cache->{$executable . ' ' . $default_subname} //=
714             $factory->($executable, $default_subname)
715             if 'CODE' ne ref $executable;
716 2164 100       5431 return $executable unless $args;
717 1     1   7 return sub { $executable->($args, @_) };
  1         4  
718             } ## end sub stock_factory
719              
720 20     20 1 49 sub stock_help ($self, $config, $args) {
  20         36  
  20         53  
  20         36  
  20         27  
721 20         68 print_help($self, get_descendant($self, $self->{trail}[-2][0], $args));
722 20         293 return 0;
723             } ## end sub stock_help
724              
725 178     178 1 484 sub stock_DefaultSources { [qw< +Default +CmdLine +Environment +Parent >] }
726              
727             sub stock_SourcesWithFiles {
728             [
729 3     3 1 12 qw< +Default +CmdLine +Environment +Parent
730             +JsonFileFromConfig +JsonFiles
731             >
732             ]
733             } ## end sub stock_SourcesWithFiles
734              
735 184     184 1 238 sub validate_configuration ($self, $spec, $args) {
  184         254  
  184         232  
  184         233  
  184         219  
736 184         297 my $from_spec = $spec->{validate};
737 184         305 my $from_self = $self->{application}{configuration}{validate};
738 184         227 my $validator;
739 184 50 33     660 if (defined $from_spec && 'HASH' ne ref $from_spec) {
    50 33        
740 0         0 $validator = $self->{factory}->($from_spec, 'validate');
741             }
742             elsif (defined $from_self && 'HASH' ne ref $from_self) {
743 0         0 $validator = $self->{factory}->($from_self, 'validate');
744             }
745             else { # use stock one
746 184         324 $validator = \¶ms_validate;
747             }
748 184         383 $validator->($self, $spec, $args);
749             } ## end sub validate_configuration
750              
751             exit run(
752             $ENV{APPEASER} // {
753             commands => {
754             MAIN => {
755             name => 'main app',
756             help => 'this is the main app',
757             description => 'Yes, this really is the main app',
758             options => [
759             {
760             name => 'foo',
761             description => 'option foo!',
762             getopt => 'foo|f=s',
763             environment => 'FOO',
764             default => 'bar',
765             },
766             ],
767             execute => sub ($global, $conf, $args) {
768             my $foo = $conf->{foo};
769             say "Hello, $foo!";
770             return 0;
771             },
772             'default-child' => '', # run execute by default
773             },
774             },
775             },
776             [@ARGV]
777             ) unless caller;
778              
779             1;