File Coverage

blib/lib/App/Spec.pm
Criterion Covered Total %
statement 194 209 92.8
branch 60 70 85.7
condition 10 17 58.8
subroutine 15 17 88.2
pod 4 4 100.0
total 283 317 89.2


line stmt bran cond sub pod time code
1             # ABSTRACT: Specification for commandline app
2 7     7   922053 use strict;
  7         51  
  7         257  
3 7     7   32 use warnings;
  7         11  
  7         451  
4             package App::Spec;
5 7     7   114 use 5.010;
  7         24  
6              
7             our $VERSION = 'v0.15.0'; # VERSION
8              
9 7     7   2864 use App::Spec::Subcommand;
  7         32  
  7         396  
10 7     7   60 use App::Spec::Option;
  7         16  
  7         217  
11 7     7   32 use App::Spec::Parameter;
  7         28  
  7         188  
12              
13 7     7   31 use Moo;
  7         13  
  7         35  
14              
15             with('App::Spec::Role::Command');
16              
17             has title => ( is => 'rw' );
18             has abstract => ( is => 'rw' );
19              
20              
21              
22              
23             sub runner {
24 33     33 1 309 my ($self, %args) = @_;
25 33         158 my $class = $self->class;
26 33         614 my $cmd = $class->new;
27 33         2134 my $run = App::Spec::Run->new({
28             spec => $self,
29             cmd => $cmd,
30             %args,
31             });
32 33         285 return $run;
33             }
34              
35             sub usage {
36 19     19 1 105 my ($self, %args) = @_;
37 19         52 my $cmds = $args{commands};
38 19 100       45 my %highlights = %{ $args{highlights} || {} };
  19         180  
39 19   33 0   91 my $colored = $args{colored} || sub { $_[1] };
  0         0  
40 19         81 my $appname = $self->name;
41              
42 19   100     102 my $abstract = $self->abstract // '';
43 19         68 my $title = $self->title;
44 19         98 my ($options, $parameters, $subcmds) = $self->_gather_options_parameters($cmds);
45 19         134 my $header = $colored->(['bold'], "$appname - $title");
46 19         73 my $usage = <<"EOM";
47             $header
48             $abstract
49              
50             EOM
51              
52 19         56 my $body = '';
53 19         65 my $usage_header = $colored->([qw/ bold /], "Usage:");
54 19         67 $usage .= "$usage_header $appname";
55 19 100       621 $usage .= " @$cmds" if @$cmds;
56 19 100       75 if (keys %$subcmds) {
57 6         16 my $maxlength = 0;
58 6         13 my @table;
59 6         13 my $usage_string = "";
60 6         12 my $header = "Subcommands:";
61 6 100       131 if ($highlights{subcommands}) {
62 3         15 $colored->([qw/ bold red /], $usage_string);
63 3         12 $colored->([qw/ bold red /], $header);
64             }
65             else {
66 3         12 $colored->([qw/ bold /], $header);
67             }
68 6         18 $usage .= " $usage_string";
69 6         15 $body .= "$header\n";
70              
71 6         10 my %keys;
72 6         60 @keys{ keys %$subcmds } = ();
73 6         17 my @keys;
74 6 50       20 if (@$cmds) {
75 0         0 @keys = sort keys %keys;
76             }
77             else {
78 6         17 for my $key (qw/ help _meta /) {
79 12 100       60 if (exists $keys{ $key }) {
80 10         19 push @keys, $key;
81 10         22 delete $keys{ $key };
82             }
83             }
84 6         49 unshift @keys, sort keys %keys;
85             }
86 6         18 for my $name (@keys) {
87 38         73 my $cmd_spec = $subcmds->{ $name };
88 38         122 my $summary = $cmd_spec->summary;
89 38         104 my @lines = split m/\n/, $summary;
90 38   50     117 push @table, [$name, $lines[0] // ''];
91 38         4165 push @table, ['', $_] for map { s/^ +//; $_ } @lines[1 .. $#lines];
  4         13  
  4         18  
92 38 100       114 if (length $name > $maxlength) {
93 16         39 $maxlength = length $name;
94             }
95             }
96 6         36 $body .= $self->_output_table(\@table, [$maxlength]);
97             }
98              
99 19 100       90 if (@$parameters) {
100 9         26 my $maxlength = 0;
101 9         25 my @table;
102             my @highlights;
103 9         22 for my $param (@$parameters) {
104 25         91 my $name = $param->name;
105 25         67 my $highlight = $highlights{parameters}->{ $name };
106 25 100       62 push @highlights, $highlight ? 1 : 0;
107 25         86 my $summary = $param->summary;
108 25         94 my $param_usage_header = $param->to_usage_header;
109 25 100       108 if ($highlight) {
110 8         34 $colored->([qw/ bold red /], $param_usage_header);
111             }
112 25         51 $usage .= " " . $param_usage_header;
113 25         60 my ($req, $multi) = (' ', ' ');
114 25 100       73 if ($param->required) {
115 24         35 $req = "*";
116             }
117 25 50       149 if ($param->mapping) {
    100          
118 0         0 $multi = '{}';
119             }
120             elsif ($param->multiple) {
121 6         11 $multi = '[]';
122             }
123              
124 25         77 my $flags = $self->_param_flags_string($param);
125              
126 25         103 my @lines = split m/\n/, $summary;
127 25   50     143 push @table, [$name, $req, $multi, ($lines[0] // '') . $flags];
128 25         87 push @table, ['', ' ', ' ', $_] for map { s/^ +//; $_ } @lines[1 .. $#lines];
  2         6  
  2         12  
129 25 100       98 if (length $name > $maxlength) {
130 14         48 $maxlength = length $name;
131             }
132             }
133 9         54 my $parameters_string = $colored->([qw/ bold /], "Parameters:");
134 9         25 $body .= "$parameters_string\n";
135 9         47 my @lines = $self->_output_table(\@table, [$maxlength]);
136 9         41 my $lines = $self->_colorize_lines(\@lines, \@highlights, $colored);
137 9         55 $body .= $lines;
138             }
139              
140 19 50       65 if (@$options) {
141 19         44 my @highlights;
142 19         45 $usage .= " [options]";
143 19         46 my $maxlength = 0;
144 19         40 my @table;
145 19         135 for my $opt (sort { $a->name cmp $b->name } @$options) {
  80         261  
146 69         181 my $name = $opt->name;
147 69         155 my $highlight = $highlights{options}->{ $name };
148 69 100       175 push @highlights, $highlight ? 1 : 0;
149 69         179 my $aliases = $opt->aliases;
150 69         215 my $summary = $opt->summary;
151             my @names = map {
152 69 100       149 length $_ > 1 ? "--$_" : "-$_"
  109         454  
153             } ($name, @$aliases);
154 69         182 my $string = "@names";
155 69 100       193 if (length $string > $maxlength) {
156 55         116 $maxlength = length $string;
157             }
158 69         145 my ($req, $multi) = (' ', ' ');
159 69 50       324 if ($opt->required) {
160 0         0 $req = "*";
161             }
162 69 100       290 if ($opt->mapping) {
    100          
163 3         7 $multi = '{}';
164             }
165             elsif ($opt->multiple) {
166 16         31 $multi = '[]';
167             }
168              
169 69         195 my $flags = $self->_param_flags_string($opt);
170              
171 69         221 my @lines = split m/\n/, $summary;
172 69   50     335 push @table, [$string, $req, $multi, ($lines[0] // '') . $flags];
173 69         308 push @table, ['', ' ', ' ', $_ ] for map { s/^ +//; $_ } @lines[1 .. $#lines];
  4         18  
  4         24  
174             }
175 19         82 my $options_string = $colored->([qw/ bold /], "Options:");
176 19         55 $body .= "\n$options_string\n";
177 19         77 my @lines = $self->_output_table(\@table, [$maxlength]);
178 19         107 my $lines = $self->_colorize_lines(\@lines, \@highlights, $colored);
179 19         130 $body .= $lines;
180             }
181              
182 19         179 return "$usage\n\n$body";
183             }
184              
185             sub _param_flags_string {
186 149     149   321 my ($self, $param) = @_;
187 149         228 my @flags;
188 149 100       477 if ($param->type eq 'flag') {
189 70         134 push @flags, "flag";
190             }
191 149 100       351 if ($param->multiple) {
192 31         61 push @flags, "multiple";
193             }
194 149 100       390 if ($param->mapping) {
195 4         11 push @flags, "mapping";
196             }
197 149 100       417 my $flags = @flags ? " (" . join("; ", @flags) . ")" : '';
198 149         380 return $flags;
199             }
200              
201             sub _colorize_lines {
202 28     28   80 my ($self, $lines, $highlights, $colored) = @_;
203 28         61 my $output = '';
204 28         121 for my $i (0 .. $#$lines) {
205 100         160 my $line = $lines->[ $i ];
206 100 100       238 if ($highlights->[ $i ]) {
207 12         45 $colored->([qw/ bold red /], $line);
208             }
209 100         239 $output .= $line;
210             }
211 28         81 return $output;
212             }
213              
214             sub _output_table {
215 34     34   92 my ($self, $table, $lengths) = @_;
216 34         75 my @lines;
217             my @lengths = map {
218 124 100       396 defined $lengths->[$_] ? "%-$lengths->[$_]s" : "%s"
219 34         69 } 0 .. @{ $table->[0] } - 1;
  34         111  
220 34         141 for my $row (@$table) {
221 7     7   20610 no warnings 'uninitialized';
  7         25  
  7         5371  
222 142         738 push @lines, sprintf join(' ', @lengths) . "\n", @$row;
223             }
224 34 100       240 return wantarray ? @lines : join '', @lines;
225             }
226              
227              
228             sub _gather_options_parameters {
229 19     19   52 my ($self, $cmds) = @_;
230 19         46 my @options;
231             my @parameters;
232 19         54 my $global_options = $self->options;
233 19         61 my $commands = $self->subcommands;
234 19         67 push @options, @$global_options;
235              
236 19         61 for my $cmd (@$cmds) {
237 13         39 my $cmd_spec = $commands->{ $cmd };
238 13   50     59 my $options = $cmd_spec->options || [];
239 13   50     54 my $parameters = $cmd_spec->parameters || [];
240 13         34 push @options, @$options;
241 13         36 push @parameters, @$parameters;
242              
243 13   100     94 $commands = $cmd_spec->subcommands || {};
244              
245             }
246 19         122 return \@options, \@parameters, $commands;
247             }
248              
249             sub generate_completion {
250 0     0 1 0 my ($self, %args) = @_;
251 0         0 my $shell = delete $args{shell};
252              
253 0 0       0 if ($shell eq "zsh") {
    0          
254 0         0 require App::Spec::Completion::Zsh;
255 0         0 my $completer = App::Spec::Completion::Zsh->new(
256             spec => $self,
257             );
258 0         0 return $completer->generate_completion(%args);
259             }
260             elsif ($shell eq "bash") {
261 0         0 require App::Spec::Completion::Bash;
262 0         0 my $completer = App::Spec::Completion::Bash->new(
263             spec => $self,
264             );
265 0         0 return $completer->generate_completion(%args);
266             }
267             }
268              
269              
270             sub make_getopt {
271 68     68 1 218 my ($self, $options, $result, $specs) = @_;
272 68         125 my @getopt;
273 68         190 for my $opt (@$options) {
274 139         492 my $name = $opt->name;
275 139         278 my $spec = $name;
276 139 50       428 if (my $aliases = $opt->aliases) {
277 139         439 $spec .= "|$_" for @$aliases;
278             }
279 139 100       493 unless ($opt->type eq 'flag') {
280 49         100 $spec .= "=s";
281             }
282 139         363 $specs->{ $name } = $opt;
283 139 100       404 if ($opt->multiple) {
284 34 100       155 if ($opt->type eq 'flag') {
    50          
285 29         83 $spec .= '+';
286             }
287             elsif ($opt->mapping) {
288 5         17 $result->{ $name } = {};
289 5         15 $spec .= '%';
290             }
291             else {
292 0         0 $result->{ $name } = [];
293 0         0 $spec .= '@';
294             }
295             }
296 139         508 push @getopt, $spec, \$result->{ $name },
297             }
298 68         391 return @getopt;
299             }
300              
301             =pod
302              
303             =head1 NAME
304              
305             App::Spec - Specification for commandline apps
306              
307             =head1 SYNOPSIS
308              
309             WARNING: This is still experimental. The spec is subject to change.
310              
311             This module represents a specification of a command line tool.
312             Currently it can read the spec from a YAML file or directly from a data
313             structure in perl.
314              
315             It uses the role L.
316              
317             The L module is the framework which will run the actual
318             app.
319              
320             Have a look at the L for how to write an app.
321              
322             In the examples directory you will find the app C which is supposed
323             to demonstrate everything that App::Spec supports right now.
324              
325             Your script:
326              
327             use App::Spec;
328             my $spec = App::Spec->read("/path/to/myapp-spec.yaml");
329              
330             my $run = $spec->runner;
331             $run->run;
332              
333             # this is equivalent to
334             #my $run = App::Spec::Run->new(
335             # spec => $spec,
336             # cmd => Your::App->new,
337             #);
338             #$run->run;
339              
340             Your App class:
341              
342             package Your::App;
343             use base 'App::Spec::Run::Cmd';
344              
345             sub command1 {
346             my ($self, $run) = @_;
347             my $options = $run->options;
348             my $param = $run->parameters;
349             # Do something
350             $run->out("Hello world!");
351             $run->err("oops");
352             # you can also use print directly
353             }
354              
355              
356             =head1 METHODS
357              
358             =over 4
359              
360             =item read
361              
362             my $spec = App::Spec->read("/path/to/myapp-spec.yaml");
363              
364             =item load_data
365              
366             Takes a file, hashref or glob and returns generated appspec hashref
367              
368             my $hash = $class->load_data($file);
369              
370             =item build
371              
372             Builds objects out of the hashref
373              
374             my $appspec = App::Spec->build(%hash);
375              
376             =item runner
377              
378             Returns an instance of the your app class
379              
380             my $run = $spec->runner;
381             $run->run;
382              
383             # this is equivalent to
384             my $run = App::Spec::Example::MyApp->new({
385             spec => $spec,
386             });
387             $run->run;
388              
389             =item usage
390              
391             Returns usage output for the specified subcommands:
392              
393             my $usage = $spec->usage(
394             commands => ["subcommand1","subcommand2"],
395             );
396              
397             =item generate_completion
398              
399             Generates shell completion script for the spec.
400              
401             my $completion = $spec->generate_completion(
402             shell => "zsh",
403             );
404              
405             =item make_getopt
406              
407             Returns options for Getopt::Long
408              
409             my @getopt = $spec->make_getopt($global_options, \%options, $option_specs);
410              
411             =item abstract, appspec, class, description, has_subcommands, markup, name, options, parameters, subcommands, title
412              
413             Accessors for the things defined in the spec (file)
414              
415             =back
416              
417             =head1 SEE ALSO
418              
419             L - Utilities for App::Spec authors
420              
421             =head1 LICENSE
422              
423             This library is free software and may be distributed under the same terms
424             as perl itself.
425              
426             =cut
427              
428             1;
429