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