File Coverage

blib/lib/App/Spec.pm
Criterion Covered Total %
statement 182 197 92.3
branch 59 70 84.2
condition 7 10 70.0
subroutine 15 17 88.2
pod 4 4 100.0
total 267 298 89.6


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