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   176664 use strict;
  5         26  
  5         125  
3 5     5   22 use warnings;
  5         9  
  5         144  
4             package App::Spec;
5 5     5   82 use 5.010;
  5         15  
6              
7             our $VERSION = '0.012'; # VERSION
8              
9 5     5   1613 use App::Spec::Subcommand;
  5         15  
  5         164  
10 5     5   32 use App::Spec::Option;
  5         9  
  5         101  
11 5     5   28 use App::Spec::Parameter;
  5         8  
  5         97  
12              
13 5     5   20 use Moo;
  5         10  
  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 259 my ($self, %args) = @_;
25 30         134 my $class = $self->class;
26 30         323 my $cmd = $class->new;
27 30         1363 my $run = App::Spec::Run->new({
28             spec => $self,
29             cmd => $cmd,
30             %args,
31             });
32 30         223 return $run;
33             }
34              
35             sub usage {
36 16     16 1 76 my ($self, %args) = @_;
37 16         43 my $cmds = $args{commands};
38 16 100       31 my %highlights = %{ $args{highlights} || {} };
  16         78  
39 16   50 0   61 my $colored = $args{colored} || sub { $_[1] };
  0         0  
40 16         59 my $appname = $self->name;
41              
42 16   100     70 my $abstract = $self->abstract // '';
43 16         61 my $title = $self->title;
44 16         54 my ($options, $parameters, $subcmds) = $self->_gather_options_parameters($cmds);
45 16         83 my $header = $colored->(['bold'], "$appname - $title");
46 16         61 my $usage = <<"EOM";
47             $header
48             $abstract
49              
50             EOM
51              
52 16         35 my $body = '';
53 16         45 my $usage_header = $colored->([qw/ bold /], "Usage:");
54 16         42 $usage .= "$usage_header $appname";
55 16 100       69 $usage .= " @$cmds" if @$cmds;
56 16 100       65 if (keys %$subcmds) {
57 4         8 my $maxlength = 0;
58 4         8 my @table;
59 4         9 my $usage_string = "";
60 4         7 my $header = "Subcommands:";
61 4 100       16 if ($highlights{subcommands}) {
62 3         11 $colored->([qw/ bold red /], $usage_string);
63 3         8 $colored->([qw/ bold red /], $header);
64             }
65             else {
66 1         4 $colored->([qw/ bold /], $header);
67             }
68 4         13 $usage .= " $usage_string";
69 4         10 $body .= "$header\n";
70              
71 4         10 my %keys;
72 4         26 @keys{ keys %$subcmds } = ();
73 4         10 my @keys;
74 4 50       11 if (@$cmds) {
75 0         0 @keys = sort keys %keys;
76             }
77             else {
78 4         104 for my $key (qw/ help _meta /) {
79 8 100       24 if (exists $keys{ $key }) {
80 7         12 push @keys, $key;
81 7         17 delete $keys{ $key };
82             }
83             }
84 4         34 unshift @keys, sort keys %keys;
85             }
86 4         80 for my $name (@keys) {
87 26         42 my $cmd_spec = $subcmds->{ $name };
88 26         59 my $summary = $cmd_spec->summary;
89 26         49 push @table, [$name, $summary];
90 26 100       65 if (length $name > $maxlength) {
91 11         22 $maxlength = length $name;
92             }
93             }
94 4         17 $body .= $self->_output_table(\@table, [$maxlength]);
95             }
96              
97 16 100       53 if (@$parameters) {
98 8         16 my $maxlength = 0;
99 8         14 my @table;
100             my @highlights;
101 8         15 for my $param (@$parameters) {
102 24         48 my $name = $param->name;
103 24         40 my $highlight = $highlights{parameters}->{ $name };
104 24 100       47 push @highlights, $highlight ? 1 : 0;
105 24         54 my $summary = $param->summary;
106 24         56 my $param_usage_header = $param->to_usage_header;
107 24 100       50 if ($highlight) {
108 8         24 $colored->([qw/ bold red /], $param_usage_header);
109             }
110 24         40 $usage .= " " . $param_usage_header;
111 24         37 my ($req, $multi) = (' ', ' ');
112 24 50       48 if ($param->required) {
113 24         30 $req = "*";
114             }
115 24 50       58 if ($param->mapping) {
    100          
116 0         0 $multi = '{}';
117             }
118             elsif ($param->multiple) {
119 6         10 $multi = '[]';
120             }
121              
122 24         49 my $flags = $self->_param_flags_string($param);
123              
124 24         68 push @table, [$name, $req, $multi, $summary . $flags];
125 24 100       67 if (length $name > $maxlength) {
126 13         27 $maxlength = length $name;
127             }
128             }
129 8         37 my $parameters_string = $colored->([qw/ bold /], "Parameters:");
130 8         21 $body .= "$parameters_string\n";
131 8         28 my @lines = $self->_output_table(\@table, [$maxlength]);
132 8         27 my $lines = $self->_colorize_lines(\@lines, \@highlights, $colored);
133 8         30 $body .= $lines;
134             }
135              
136 16 50       58 if (@$options) {
137 16         29 my @highlights;
138 16         33 $usage .= " [options]";
139 16         30 my $maxlength = 0;
140 16         30 my @table;
141 16         73 for my $opt (sort { $a->name cmp $b->name } @$options) {
  61         142  
142 58         99 my $name = $opt->name;
143 58         94 my $highlight = $highlights{options}->{ $name };
144 58 100       107 push @highlights, $highlight ? 1 : 0;
145 58         93 my $aliases = $opt->aliases;
146 58         110 my $summary = $opt->summary;
147             my @names = map {
148 58 100       92 length $_ > 1 ? "--$_" : "-$_"
  94         291  
149             } ($name, @$aliases);
150 58         109 my $string = "@names";
151 58 100       125 if (length $string > $maxlength) {
152 47         60 $maxlength = length $string;
153             }
154 58         98 my ($req, $multi) = (' ', ' ');
155 58 50       127 if ($opt->required) {
156 0         0 $req = "*";
157             }
158 58 100       162 if ($opt->mapping) {
    100          
159 3         4 $multi = '{}';
160             }
161             elsif ($opt->multiple) {
162 15         31 $multi = '[]';
163             }
164              
165 58         98 my $flags = $self->_param_flags_string($opt);
166              
167 58         212 push @table, [$string, $req, $multi, $summary . $flags];
168             }
169 16         55 my $options_string = $colored->([qw/ bold /], "Options:");
170 16         49 $body .= "\n$options_string\n";
171 16         57 my @lines = $self->_output_table(\@table, [$maxlength]);
172 16         70 my $lines = $self->_colorize_lines(\@lines, \@highlights, $colored);
173 16         68 $body .= $lines;
174             }
175              
176 16         121 return "$usage\n\n$body";
177             }
178              
179             sub _param_flags_string {
180 82     82   132 my ($self, $param) = @_;
181 82         92 my @flags;
182 82 100       169 if ($param->type eq 'flag') {
183 38         58 push @flags, "flag";
184             }
185 82 100       185 if ($param->multiple) {
186 24         40 push @flags, "multiple";
187             }
188 82 100       144 if ($param->mapping) {
189 3         6 push @flags, "mapping";
190             }
191 82 100       183 my $flags = @flags ? " (" . join("; ", @flags) . ")" : '';
192 82         149 return $flags;
193             }
194              
195             sub _colorize_lines {
196 24     24   60 my ($self, $lines, $highlights, $colored) = @_;
197 24         31 my $output = '';
198 24         80 for my $i (0 .. $#$lines) {
199 82         102 my $line = $lines->[ $i ];
200 82 100       148 if ($highlights->[ $i ]) {
201 12         31 $colored->([qw/ bold red /], $line);
202             }
203 82         148 $output .= $line;
204             }
205 24         74 return $output;
206             }
207              
208             sub _output_table {
209 28     28   58 my ($self, $table, $lengths) = @_;
210 28         39 my @lines;
211             my @lengths = map {
212 104 100       246 defined $lengths->[$_] ? "%-$lengths->[$_]s" : "%s"
213 28         53 } 0 .. @{ $table->[0] } - 1;
  28         76  
214 28         71 for my $row (@$table) {
215 5     5   7488 no warnings 'uninitialized';
  5         16  
  5         2533  
216 108         489 push @lines, sprintf join(' ', @lengths) . "\n", @$row;
217             }
218 28 100       132 return wantarray ? @lines : join '', @lines;
219             }
220              
221              
222             sub _gather_options_parameters {
223 16     16   42 my ($self, $cmds) = @_;
224 16         36 my @options;
225             my @parameters;
226 16         38 my $global_options = $self->options;
227 16         49 my $commands = $self->subcommands;
228 16         43 push @options, @$global_options;
229              
230 16         46 for my $cmd (@$cmds) {
231 12         25 my $cmd_spec = $commands->{ $cmd };
232 12   50     45 my $options = $cmd_spec->options || [];
233 12   50     37 my $parameters = $cmd_spec->parameters || [];
234 12         25 push @options, @$options;
235 12         23 push @parameters, @$parameters;
236              
237 12   100     57 $commands = $cmd_spec->subcommands || {};
238              
239             }
240 16         54 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 144 my ($self, $options, $result, $specs) = @_;
266 61         89 my @getopt;
267 61         134 for my $opt (@$options) {
268 119         238 my $name = $opt->name;
269 119         170 my $spec = $name;
270 119 50       301 if (my $aliases = $opt->aliases) {
271 119         325 $spec .= "|$_" for @$aliases;
272             }
273 119 100       337 unless ($opt->type eq 'flag') {
274 38         69 $spec .= "=s";
275             }
276 119         250 $specs->{ $name } = $opt;
277 119 100       263 if ($opt->multiple) {
278 33 100       125 if ($opt->type eq 'flag') {
    50          
279 28         69 $spec .= '+';
280             }
281             elsif ($opt->mapping) {
282 5         16 $result->{ $name } = {};
283 5         17 $spec .= '%';
284             }
285             else {
286 0         0 $result->{ $name } = [];
287 0         0 $spec .= '@';
288             }
289             }
290 119         341 push @getopt, $spec, \$result->{ $name },
291             }
292 61         203 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