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   59225 use v5.24;
  22         72  
3 22     22   90 use warnings;
  22         105  
  22         518  
4 22     22   479 use experimental qw< signatures >;
  22         2921  
  22         99  
5 22     22   2100 no warnings qw< experimental::signatures >;
  22         50  
  22         1188  
6             { our $VERSION = '2.007' }
7              
8 22     22   1838 use parent 'Exporter';
  22         1243  
  22         122  
9             our @EXPORT_OK = qw< d run appeaser_api >;
10              
11 103     103 1 146 sub add_auto_commands ($application) {
  103         131  
  103         127  
12 103         215 my $commands = $application->{commands};
13             $commands->{help} //= {
14 103   100     451 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     367 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         176 return $application;
32             } ## end sub add_auto_commands ($application)
33              
34 3     3 1 3213 sub appeaser_api { return 'V1' }
35              
36 183     183 1 205 sub collect ($self, $spec, $args) {
  183         225  
  183         197  
  183         195  
  183         188  
37 183         200 my @sequence;
38 183         253 my $config = {};
39 183         207 my @residual_args;
40              
41 183         351 my $merger = merger($self, $spec);
42              
43 183         372 for my $source_spec (sources($self, $spec, $args)) {
44 734 50       1639 my ($src, $src_cnf) =
45             'ARRAY' eq ref $source_spec
46             ? $source_spec->@*
47             : ($source_spec, {});
48 734         1188 $src = $self->{factory}->($src, 'collect'); # "resolve"
49 734         3052 $src_cnf = {$spec->%*, $src_cnf->%*, config => $config};
50 734         1798 my ($slice, $residual_args) = $src->($self, $src_cnf, $args);
51 734 100       1748 push @residual_args, $residual_args->@* if defined $residual_args;
52 734         943 push @sequence, $slice;
53 734         1114 $config = $merger->(@sequence);
54             } ## end for my $source_spec (sources...)
55              
56 183         574 return ($config, \@residual_args);
57             } ## end sub collect
58              
59 184     184 1 208 sub collect_options ($self, $spec, $args) {
  184         223  
  184         200  
  184         200  
  184         204  
60 184         245 my $factory = $self->{factory};
61             my $collect = $spec->{collect}
62 184   66     821 // $self->{application}{configuration}{collect} // \&collect;
      100        
63 184         301 my $collector = $factory->($collect, 'collect'); # "resolve"
64 184         354 (my $config, $args) = $collector->($self, $spec, $args);
65 184         343 push $self->{configs}->@*, $config;
66 184         308 return $args;
67             } ## end sub collect_options
68              
69 25     25 1 32 sub commandline_help ($getopt) {
  25         48  
  25         32  
70 25         32 my @retval;
71              
72 25         44 my ($mode, $type, $desttype, $min, $max, $default);
73 25 100       219 if (substr($getopt, -1, 1) eq '!') {
    50          
    50          
    0          
    0          
74 10         18 $type = 'bool';
75 10         24 substr $getopt, -1, 1, '';
76 10         47 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       66 $mode = $1 eq '=' ? 'mandatory' : 'optional';
99 15         51 $type = $2;
100 15         46 $desttype = $3;
101 15         28 $min = $4;
102 15         22 $max = $5;
103 15 50       40 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         185 }->{$type};
112 15         67 my $line = "$mode $type option";
113 15 50 33     90 $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     170 $line .= ", list valued" if defined($desttype) && $desttype eq '@';
117 15         41 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         75 my @alternatives = split /\|/, $getopt;
139 25 100       103 if ($type eq 'bool') {
    50          
140             push @retval, map {
141 10 100       32 if (length($_) == 1) { "-$_" }
  18         47  
  8         34  
142 10         37 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       34 if (length($_) == 1) { "-$_ " }
  30         111  
  15         44  
154 15         96 else { "--$_ " }
155             } @alternatives;
156             } ## end else [ if ($type eq 'bool') ]
157              
158 25         81 return @retval;
159             } ## end sub commandline_help ($getopt)
160              
161 184     184 1 198 sub commit_configuration ($self, $spec, $args) {
  184         225  
  184         196  
  184         194  
  184         186  
162 184   50     333 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   26623 no warnings;
  22         44  
  22         123382  
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 224 sub default_getopt_config ($self, $spec) {
  183         212  
  183         206  
  183         188  
174 183         298 my @r = qw< gnu_getopt >;
175 183 100       326 push @r, qw< require_order pass_through >
176             if has_children($self, $spec);
177 183 50       373 push @r, qw< pass_through > if $spec->{'allow-residual-options'};
178 183         401 return \@r;
179             }
180              
181 93     93 1 140 sub execute ($self, $args) {
  93         128  
  93         108  
  93         111  
182 93         154 my $command = $self->{trail}[-1][0];
183             my $executable = fetch_spec_for($self, $command)->{execute}
184 93 50       157 or die "no executable for '$command'\n";
185 93         200 $executable = $self->{factory}->($executable, 'execute'); # "resolve"
186 93   50     232 my $config = $self->{configs}[-1] // {};
187 93         280 return $executable->($self, $config, $args);
188             } ## end sub execute
189              
190 37     37 1 53 sub fetch_subcommand_default ($self, $spec) {
  37         46  
  37         44  
  37         42  
191 37         59 my $acfg = $self->{application}{configuration};
192             my $child = exists($spec->{'default-child'}) ? $spec->{'default-child'}
193 37 0       94 : exists($acfg->{'default-child'}) ? $acfg->{'default-child'}
    50          
194             : get_child($self, $spec, 'help'); # help is last resort
195 37 100 100     192 return ($child, $child) if defined $child && length $child;
196 23         109 return;
197             }
198              
199 184     184 1 193 sub fetch_subcommand ($self, $spec, $args) {
  184         206  
  184         208  
  184         190  
  184         222  
200 184 100       401 my ($subc, $alias) = fetch_subcommand_wh($self, $spec, $args)
201             or return;
202 85         143 my $r = ref $subc;
203 85 100       175 if ($r eq 'HASH') {
204             $subc = $spec->{children}[$subc->{index}]
205 3 100 66     14 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       185 die "invalid sub-command (ref to $r)" if $r;
211 83         232 return ($subc, $alias);
212             }
213              
214 184     184 1 218 sub fetch_subcommand_wh ($self, $spec, $args) {
  184         197  
  184         186  
  184         200  
  184         191  
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         327 for my $cfg ($spec, $self->{application}{configuration}) {
218 366 100       663 next unless exists $cfg->{dispatch};
219 2         6 my $sub = $self->{factory}->($cfg->{dispatch}, 'dispatch');
220 2 50       7 defined(my $child = $sub->($self, $spec, $args)) or return;
221 2         12 return ($child, $child);
222             }
223              
224             # regular course here, no point in going forth without children
225 182 100       302 return unless has_children($self, $spec);
226              
227             # use defaults if there's no argument to investigate
228 112 100       275 return fetch_subcommand_default($self, $spec) unless $args->@*;
229              
230             # try to get a child from the first argument
231 77 100       189 if (my $child = get_child($self, $spec, $args->[0])) {
232 65         212 return ($child, shift $args->@*); # consumed arg name
233             }
234              
235             # the first argument didn't help, but we might want to fallback
236 12         28 for my $cfg ($spec, $self->{application}{configuration}) {
237 18 100       39 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         13 return ($child, $child);
242             }
243 16 100       37 if (exists $spec->{'fallback-to'}) {
244 2 50       7 defined(my $fbto = $spec->{'fallback-to'}) or return;
245 2         7 return ($fbto, $fbto);
246             }
247             return fetch_subcommand_default($self, $spec)
248 14 100       37 if $cfg->{'fallback-to-default'};
249             }
250              
251             # no fallback at this point... it's an error, build a message and die!
252 6         17 my @names = map { $_->[1] } $self->{trail}->@*;
  6         17  
253 6         13 shift @names; # remove first one
254 6         22 my $path = join '/', @names, $args->[0]; # $args->[0] was the candidate
255 6         72 die "cannot find sub-command '$path'\n";
256             } ## end sub fetch_subcommand_wh
257              
258 104     104 1 220 sub generate_factory ($c) {
  104         154  
  104         126  
259 104         185 my $w = \&stock_factory; # default factory
260 104 50       284 $w = stock_factory($c->{create}, 'factory', $c) if defined $c->{create};
261 104     2166   1048 return sub ($e, $d = '') { $w->($e, $d, $c) };
  2166         2075  
  2166         2907  
  2166         7880  
  2166         2371  
  2166         2330  
262             }
263              
264 88     88 1 130 sub get_child ($self, $spec, $name) {
  88         119  
  88         97  
  88         119  
  88         105  
265 88         163 for my $child (get_children($self, $spec)) {
266 157         281 my $command = fetch_spec_for($self, $child);
267             next
268 199         488 unless grep { $_ eq $name }
269 157 100 100     431 ($command->{supports} //= [$child])->@*;
270 76         241 return $child;
271             } ## end for my $child (get_children...)
272 12         34 return;
273             } ## end sub get_child
274              
275 10     10 1 14 sub stock_ChildrenByPrefix ($self, $spec, @prefixes) {
  10         10  
  10         11  
  10         22  
  10         10  
276 10         46 require File::Spec;
277             my @expanded_inc = map {
278 10         20 my ($v, $dirs) = File::Spec->splitpath($_, 'no-file');
  90         289  
279 90         401 [$v, File::Spec->splitdir($dirs)];
280             } @INC;
281 10         15 my %seen;
282             return map {
283 10         13 my @parts = split m{::}mxs, $_ . 'x';
  10         25  
284 10         23 substr(my $bprefix = pop @parts, -1, 1, '');
285             map {
286 10         14 my ($v, @dirs) = $_->@*;
  90         243  
287 90         537 my $dirs = File::Spec->catdir(@dirs, @parts);
288 90 100       1549 if (opendir my $dh, File::Spec->catpath($v, $dirs, '')) {
289 18         171 grep { ! $seen{$_}++ }
290             map {
291 18         34 substr(my $lastpart = $_, -3, 3, '');
292 18         42 join '::', @parts, $lastpart;
293             } grep {
294 10         198 my $path = File::Spec->catpath($v, $dirs, $_);
  46         266  
295 46 100 66     916 (-e $path && ! -d $path)
      66        
296             && substr($_, 0, length($bprefix)) eq $bprefix
297             && substr($_, -3, 3) eq '.pm'
298             } readdir $dh;
299             }
300 80         371 else { () }
301             } @expanded_inc;
302             } @prefixes;
303             }
304              
305 126     126 1 130 sub expand_children ($self, $spec, $child_spec) {
  126         140  
  126         129  
  126         131  
  126         133  
306 126 100       341 return $child_spec unless ref($child_spec) eq 'ARRAY';
307 10         22 my ($exe, @args) = $child_spec->@*;
308 10         17 return $self->{factory}->($exe, 'children')->($self, $spec, @args);
309             }
310              
311 482     482 1 506 sub get_children ($self, $spec, $expand = 1) {
  482         493  
  482         482  
  482         551  
  482         482  
312 482 100       967 return if $spec->{leaf};
313 411 100 100     1200 return if exists($spec->{children}) && !$spec->{children};
314 388   100     992 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       765 unless exists $self->{application}{configuration}{'auto-leaves'};
319              
320             return
321 388 100 100     1159 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       599 @children = map { expand_children($self, $spec, $_) } @children
  126         220  
326             if $expand;
327              
328             my @auto =
329             exists $self->{application}{configuration}{'auto-children'}
330 333 50 0     782 ? (($self->{application}{configuration}{'auto-children'} // [])->@*)
331             : (qw< help commands >);
332 333 50       529 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         1063 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 53 sub get_descendant ($self, $start, $list) {
  27         40  
  27         37  
  27         48  
  27         35  
350 27         34 my $target = $start;
351 27         38 my $path;
352 27         55 for my $desc ($list->@*) {
353 11 50       39 $path = defined($path) ? "$path/$desc" : $desc;
354 11 50       22 my $command = fetch_spec_for($self, $target)
355             or die "cannot find sub-command '$path'\n";
356 11 50       37 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       50 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 397 sub has_children ($self, $spec) { get_children($self, $spec, 0) ? 1 : 0 }
  365         404  
  365         373  
  365         358  
  365         635  
366              
367             sub hash_merge {
368 737     737 1 876 my (%retval, %is_overridable);
369 737         1062 for my $href (@_) {
370 1850         2889 for my $src_key (keys $href->%*) {
371 638         762 my $dst_key = $src_key;
372 638         640 my $this_overridable;
373 638 100       1636 if ($dst_key =~ m{\A //= (.*) \z}mxs) { # overridable
374 354         652 $dst_key = $1;
375 354 50       716 $is_overridable{$dst_key} = 1 unless exists $retval{$dst_key};
376 354         431 $this_overridable = 1;
377             }
378             $retval{$dst_key} = $href->{$src_key}
379 638 100 100     1737 if $is_overridable{$dst_key} || ! exists($retval{$dst_key});
380 638 100       1361 $is_overridable{$dst_key} = 0 unless $this_overridable;
381             }
382             }
383 737         2091 return \%retval;
384             # was a simple: return {map { $_->%* } reverse @_};
385             }
386              
387 19     19 1 60 sub list_commands ($self, $children) {
  19         36  
  19         30  
  19         29  
388 19         28 my $retval = '';
389 19     10   385 open my $fh, '>', \$retval;
  10         56  
  10         16  
  10         59  
390 19         5772 for my $child ($children->@*) {
391 49         82 my $command = fetch_spec_for($self, $child);
392 49         98 my $help = $command->{help};
393 49   50     123 my @aliases = ($command->{supports} // [$child])->@*;
394 49 50       75 next unless @aliases;
395 49         60 printf {$fh} "%15s: %s\n", shift(@aliases), $help;
  49         206  
396 49 100       122 printf {$fh} "%15s (also as: %s)\n", '', join ', ', @aliases
  2         10  
397             if @aliases;
398             } ## end for my $child ($children...)
399 19         52 close $fh;
400 19         526 return $retval;
401             } ## end sub list_commands
402              
403 109     109 1 325 sub load_application ($application) {
  109         189  
  109         136  
404 109 100       462 return $application if 'HASH' eq ref $application;
405              
406 5         6 my $text;
407 5 100       10 if ('SCALAR' eq ref $application) {
408 2         5 $text = $$application;
409             }
410             else {
411             my $fh =
412             'GLOB' eq ref $application
413             ? $application
414 3 100       8 : do {
415 2 50       61 open my $fh, '<:encoding(UTF-8)', $application
416             or die "cannot open '$application'\n";
417 2         9115 $fh;
418             };
419 3         11 local $/; # slurp mode
420 3         74 $text = <$fh>;
421 3         70 close $fh;
422             } ## end else [ if ('SCALAR' eq ref $application)]
423              
424             return eval {
425 5         543 require JSON::PP;
426 5         11615 JSON::PP::decode_json($text);
427 5   66     8 } // eval { eval $text; } // die "cannot load application\n";
  3   50     1537  
428             } ## end sub load_application ($application)
429              
430 186     186 1 480 sub merger ($self, $spec = {}) {
  186         224  
  186         227  
  186         192  
431             my $merger = $spec->{merge}
432 186   33     819 // $self->{application}{configuration}{merge} // \&hash_merge;
      50        
433 186         335 return $self->{factory}->($merger, 'merge'); # "resolve"
434             }
435              
436 204     204 1 235 sub env_namer ($self, $cspec) {
  204         233  
  204         248  
  204         214  
437             my $namenv = $cspec->{namenv}
438 204   33     942 // $self->{application}{configuration}{namenv} // \&stock_NamEnv;
      50        
439 204         358 $namenv = $self->{factory}->($namenv, 'namenv'); # "resolve"
440 204     263   772 return sub ($ospec) { $namenv->($self, $cspec, $ospec) };
  263         267  
  263         425  
  263         292  
  263         286  
441             } ## end sub name_for_option ($o)
442              
443 182     182 1 208 sub name_for_option ($o) {
  182         337  
  182         384  
444 182 50       374 return $o->{name} if defined $o->{name};
445 182 50 33     1797 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 200 sub params_validate ($self, $spec, $args) {
  184         200  
  184         199  
  184         208  
  184         185  
452             my $validator = $spec->{validate}
453 184   33     651 // $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 40 sub print_commands ($self, $target) {
  8         10  
  8         14  
  8         11  
459 8         22 my $command = fetch_spec_for($self, $target);
460             my $fh =
461 8 50       35 $self->{application}{configuration}{'help-on-stderr'}
462             ? \*STDERR
463             : \*STDOUT;
464 8 100       23 if (my @children = get_children($self, $command)) {
465 7         14 print {$fh} list_commands($self, \@children);
  7         20  
466             }
467             else {
468 1         1 print {$fh} "no sub-commands\n";
  1         43  
469             }
470             }
471              
472 21     21 1 58 sub print_help ($self, $target) {
  21         30  
  21         36  
  21         25  
473 21         51 my $command = fetch_spec_for($self, $target);
474 21         50 my $enamr = env_namer($self, $command);
475             my $fh =
476 21 50       85 $self->{application}{configuration}{'help-on-stderr'}
477             ? \*STDERR
478             : \*STDOUT;
479              
480 21         35 print {$fh} $command->{help}, "\n\n";
  21         900  
481              
482 21 100       129 if (defined(my $description = $command->{description})) {
483 20         135 $description =~ s{\A\s+|\s+\z}{}gmxs; # trim
484 20         93 $description =~ s{^}{ }gmxs; # add some indentation
485 20         38 print {$fh} "Description:\n$description\n\n";
  20         280  
486             }
487              
488 13         177 printf {$fh} "Can be called as: %s\n\n", join ', ',
489             $command->{supports}->@*
490 21 100       128 if $command->{supports};
491              
492 21   100     103 my $options = $command->{options} // [];
493 21 100       77 if ($options->@*) {
494 15         25 print {$fh} "Options:\n";
  15         135  
495 15         45 my $n = 0; # count the option
496 15         38 for my $option ($options->@*) {
497 25 100       72 print {$fh} "\n" if $n++;
  10         84  
498              
499 25         63 printf {$fh} "%15s: %s\n", name_for_option($option),
500 25   50     51 $option->{help} // '';
501              
502 25 50       141 if (exists $option->{getopt}) {
503 25         87 my @lines = commandline_help($option->{getopt});
504 25         41 printf {$fh} "%15s command-line: %s\n", '', shift(@lines);
  25         342  
505 25         129 printf {$fh} "%15s %s\n", '', $_ for @lines;
  48         539  
506             }
507              
508 25 100       80 if (defined(my $env_name = $enamr->($option))) {
509 16         30 printf {$fh} "%15s environment : %s\n", '', $env_name;
  16         185  
510             }
511              
512 8         106 printf {$fh} "%15s default : %s\n", '',
513             $option->{default} // '*undef*'
514 25 100 50     109 if exists $option->{default};
515             } ## end for my $option ($options...)
516 15         32 print {$fh} "\n";
  15         168  
517             } ## end if ($options->@*)
518             else {
519 6         12 print {$fh} "This command has no options.\n\n";
  6         58  
520             }
521              
522 21 100       96 if (my @children = get_children($self, $command)) {
523 12         20 print {$fh} "Sub commands:\n", list_commands($self, \@children),
  12         46  
524             "\n";
525             }
526             else {
527 9         16 print {$fh} "no sub-commands\n\n";
  9         115  
528             }
529             }
530              
531 403     403 1 417 sub stock_SpecFromHash ($s, $cmd) {
  403         416  
  403         432  
  403         374  
532 403 100       633 return $cmd if ref($cmd) eq 'HASH';
533 390   100     1181 return $s->{application}{commands}{$cmd} // undef;
534             }
535              
536 151     151 1 159 sub stock_SpecFromHashOrModule ($s, $cmd) {
  151         150  
  151         162  
  151         147  
537 151 50       251 return $cmd if ref($cmd) eq 'HASH';
538             return $s->{application}{commands}{$cmd}
539 151   66     416 //= $s->{factory}->($cmd, 'spec')->();
540             }
541              
542 554     554 1 601 sub fetch_spec_for ($self, $command) {
  554         583  
  554         597  
  554         541  
543             my $fetcher = $self->{application}{configuration}{specfetch}
544 554   100     1388 // \&stock_SpecFromHash;
545 554         935 return $self->{factory}->($fetcher, 'specfetch')->($self, $command);
546             }
547              
548 103     103 1 380990 sub run ($application, $args) {
  103         176  
  103         136  
  103         135  
549 103         273 $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     643 trail => [['MAIN', $application->{commands}{MAIN}{name}]],
559             };
560              
561 103         184 while ('necessary') {
562 188         323 my $command = $self->{trail}[-1][0];
563 188 100       332 my $spec = fetch_spec_for($self, $command)
564             or die "no definition for '$command'\n";
565              
566 184         376 $args = collect_options($self, $spec, $args);
567 184         446 validate_configuration($self, $spec, $args);
568 184         424 commit_configuration($self, $spec, $args);
569              
570 184 100       386 my ($subc, $alias) = fetch_subcommand($self, $spec, $args) or last;
571 85         334 push $self->{trail}->@*, [$subc, $alias];
572             } ## end while ('necessary')
573              
574 93   50     247 return execute($self, $args) // 0;
575             } ## end sub run
576              
577 2     2 1 4 sub slurp ($file, $mode = '<:encoding(UTF-8)') {
  2         3  
  2         3  
  2         3  
578 2 50       62 open my $fh, $mode, $file or die "open('$file'): $!\n";
579 2         9407 local $/;
580 2         71 return <$fh>;
581             }
582              
583 183     183 1 253 sub sources ($self, $spec, $args) {
  183         237  
  183         217  
  183         214  
  183         222  
584             my $s = $spec->{sources}
585             // $self->{application}{configuration}{sources}
586 183   100     787 // \&stock_DefaultSources;
      100        
587 183 100       491 $s = $self->{factory}->($s, 'sources')->() if 'ARRAY' ne ref $s;
588 183         533 return $s->@*;
589             } ## end sub sources
590              
591 183     183 1 245 sub stock_CmdLine ($self, $spec, $args) {
  183         206  
  183         189  
  183         214  
  183         187  
592 183         312 my @args = $args->@*;
593 183   33     572 my $goc = $spec->{'getopt-config'}
594             // default_getopt_config($self, $spec);
595 183         10528 require Getopt::Long;
596 183         136631 Getopt::Long::Configure('default', $goc->@*);
597              
598 183         9714 my %option_for;
599             my @specs = map {
600 238         322 my $go = $_->{getopt};
601             ref($go) eq 'ARRAY'
602 0     0   0 ? ($go->[0] => sub { $go->[1]->(\%option_for, @_) })
603 238 50       570 : $go;
604             }
605 183   100     568 grep { exists $_->{getopt} } ($spec->{options} // [])->@*;
  238         515  
606 183 50       615 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         28713 my $strict = !$spec->{'allow-residual-options'};
611 183 50 66     971 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         556 return (\%option_for, \@args);
618             } ## end sub stock_CmdLine
619              
620 3     3 1 6 sub stock_JsonFileFromConfig ($self, $spec, $args) {
  3         3  
  3         4  
  3         3  
  3         2  
621 3   50     10 my $key = $spec->{'config-option'} // 'config';
622 3 100       9 return {} if !exists($spec->{config}{$key});
623 1         622 require JSON::PP;
624 1         11397 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         5  
  3         5  
628             return merger($self, $spec)->(
629             map {
630 1         7 require JSON::PP;
631 1         4 JSON::PP::decode_json(slurp($_));
632             }
633 3   100     19 grep { -e $_ } ($spec->{'config-files'} // [])->@*
  2         86  
634             );
635             } ## end sub stock_JsonFiles
636              
637 181     181 1 250 sub stock_Default ($self, $spec, @ignore) {
  181         216  
  181         194  
  181         244  
  181         192  
638             return {
639 87         205 map { '//=' . name_for_option($_) => $_->{default} }
640 181   100     569 grep { exists $_->{default} } ($spec->{options} // [])->@*
  230         558  
641             };
642             } ## end sub stock_Default
643              
644 183     183 1 220 sub stock_Environment ($self, $spec, @ignore) {
  183         217  
  183         206  
  183         244  
  183         192  
645 183         341 my $enamr = env_namer($self, $spec);
646             return {
647             map {
648 238         377 my $en = $enamr->($_); # name of environment variable
649             defined($en) && exists($ENV{$en})
650 238 100 100     1166 ? (name_for_option($_) => $ENV{$en}) : ();
651 183   100     638 } ($spec->{options} // [])->@*
652             };
653             } ## end sub stock_Environment
654              
655 263     263 1 271 sub stock_NamEnv ($self, $cspec, $ospec) {
  263         294  
  263         313  
  263         274  
  263         260  
656 263         294 my $aek = 'auto-environment';
657             my $autoenv = exists $cspec->{$aek} ? $cspec->{$aek}
658 263 100 100     813 : $self->{application}{configuration}{$aek} // undef;
659             my $env = exists $ospec->{environment} ? $ospec->{environment}
660 263 100       531 : $autoenv ? 1 : undef;
    100          
661 263 100 100     768 return $env unless ($env // '') eq '1';
662 36   50     92 my $appname = $self->{application}{configuration}{name} // '';
663 36         54 my $optname = name_for_option($ospec);
664 36         117 return uc(join '_', $appname, $optname);
665             }
666              
667 181   100 181 1 221 sub stock_Parent ($self, $spec, @ignore) { $self->{configs}[-1] // {} }
  181         211  
  181         205  
  181         239  
  181         195  
  181         621  
668              
669 7     7 1 12 sub stock_commands ($self, $config, $args) {
  7         14  
  7         12  
  7         10  
  7         12  
670 7 50       22 die "this command does not support arguments\n" if $args->@*;
671 7         22 my $target = get_descendant($self, $self->{trail}[-2][0], $args);
672 7         25 print_commands($self, $target);
673 7         98 return 0;
674             } ## end sub stock_commands
675              
676 2166     2166 1 2072 sub stock_factory ($executable, $default_subname = '', $opts = {}) {
  2166         2229  
  2166         2442  
  2166         2153  
  2166         2019  
677 90     90   102 state $factory = sub ($executable, $default_subname) {
  90         123  
  90         113  
  90         118  
678             my @prefixes =
679             !defined $opts->{prefixes} ? ()
680             : 'ARRAY' eq ref $opts->{prefixes} ? ($opts->{prefixes}->@*)
681 90 50       229 : ($opts->{prefixes});
    100          
682 90         213 push @prefixes, {'+' => 'App::Easer::V1#stock_'};
683             SEARCH:
684 90         171 for my $expansion_for (@prefixes) {
685 96         233 for my $p (keys $expansion_for->%*) {
686 111 100       297 next if $p ne substr $executable, 0, length $p;
687 74         163 substr $executable, 0, length $p, $expansion_for->{$p};
688 74         136 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       350 return eval $executable if $executable =~ s{\A \s* = \s* }{}mxs;
694              
695 89         280 my ($package, $sname) = split m{\#}mxs, $executable;
696 89 100 66     361 $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       703 if (my $s = $package->can($sname)) { return $s }
  83         333  
700              
701             # otherwise force loading of $package and retry
702 6         25 (my $path = "$package.pm") =~ s{::}{/}gmxs;
703 6         1758 require $path;
704 4 50       69 if (my $s = $package->can($sname)) { return $s }
  4         20  
705              
706 0         0 die "no '$sname' in '$package'\n";
707 2166         2294 };
708 2166         2094 state $cache = {};
709              
710 2166         2112 my $args;
711 2166 100       3399 ($executable, $args) = ($executable->{executable}, $executable)
712             if 'HASH' eq ref $executable;
713 2166 100 100     4435 $executable = $cache->{$executable . ' ' . $default_subname} //=
714             $factory->($executable, $default_subname)
715             if 'CODE' ne ref $executable;
716 2164 100       4460 return $executable unless $args;
717 1     1   5 return sub { $executable->($args, @_) };
  1         3  
718             } ## end sub stock_factory
719              
720 20     20 1 34 sub stock_help ($self, $config, $args) {
  20         30  
  20         35  
  20         35  
  20         24  
721 20         68 print_help($self, get_descendant($self, $self->{trail}[-2][0], $args));
722 20         236 return 0;
723             } ## end sub stock_help
724              
725 178     178 1 399 sub stock_DefaultSources { [qw< +Default +CmdLine +Environment +Parent >] }
726              
727             sub stock_SourcesWithFiles {
728             [
729 3     3 1 9 qw< +Default +CmdLine +Environment +Parent
730             +JsonFileFromConfig +JsonFiles
731             >
732             ]
733             } ## end sub stock_SourcesWithFiles
734              
735 184     184 1 204 sub validate_configuration ($self, $spec, $args) {
  184         227  
  184         201  
  184         232  
  184         189  
736 184         264 my $from_spec = $spec->{validate};
737 184         260 my $from_self = $self->{application}{configuration}{validate};
738 184         209 my $validator;
739 184 50 33     600 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         274 $validator = \¶ms_validate;
747             }
748 184         341 $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;