File Coverage

blib/lib/CLI/Driver.pm
Criterion Covered Total %
statement 217 303 71.6
branch 38 122 31.1
condition 5 18 27.7
subroutine 45 50 90.0
pod n/a
total 305 493 61.8


line stmt bran cond sub pod time code
1             package CLI::Driver;
2              
3             =head1 NAME
4              
5             CLI::Driver - Drive your cli tool with YAML
6              
7             =cut
8              
9 18     18   784298 use Modern::Perl;
  18         8519  
  18         131  
10 18     18   12379 use Moose;
  18         7955237  
  18         124  
11 18     18   138059 use namespace::autoclean;
  18         140035  
  18         74  
12 18     18   9578 use Kavorka 'method';
  18         175516  
  18         156  
13 18     18   3281484 use Data::Printer alias => 'pdump';
  18         32186  
  18         188  
14 18     18   11122 use CLI::Driver::Action;
  18         75  
  18         945  
15 18     18   165 use Module::Load;
  18         39  
  18         171  
16              
17 18     18   1257 use Getopt::Long;
  18         37  
  18         164  
18             Getopt::Long::Configure('no_ignore_case');
19             Getopt::Long::Configure('pass_through');
20             Getopt::Long::Configure('no_auto_abbrev');
21              
22 18     18   3573 use YAML::Syck;
  18         59  
  18         1840  
23             $YAML::Syck::ImplicitTyping = 1;
24              
25             with 'CLI::Driver::CommonRole';
26              
27             our $VERSION = 0.76;
28              
29             =head1 SYNOPSIS
30              
31             use CLI::Driver;
32            
33             my $cli = CLI::Driver->new;
34             $cli->run;
35              
36             - or -
37            
38             my $cli = CLI::Driver->new(
39             path => './etc:/etc',
40             file => 'myconfig.yml'
41             );
42             $cli->run;
43            
44             - or -
45              
46             my $cli = CLI::Driver->new(
47             use_file_sharedir => 1,
48             file_sharedir_dist_name => 'CLI-Driver',
49             );
50             $cli->run;
51            
52             #################################
53             # cli-driver.yml example
54             #################################
55             do-something:
56             desc: "Action description"
57             deprecated:
58             status: false
59             replaced-by: na
60             class:
61             name: My::App
62             attr:
63             required:
64             hard:
65             f: foo
66             soft:
67             h: home
68             a: '@array_arg'
69             optional:
70             flags:
71             dry-run: dry_run_flag
72             method:
73             name: my_method
74             args:
75             required:
76             hard:
77             soft:
78             optional:
79             flags:
80             help:
81             args:
82             f: "Additional help info for argument 'f'"
83             examples:
84             - "-f foo -a val1 -a val2 --dry-run"
85            
86             =cut
87              
88             ##############################################################################
89             ### CONSTANTS
90             ##############################################################################
91              
92 18     18   112 use constant DEFAULT_CLI_DRIVER_PATH => ( '.', 'etc', '/etc' );
  18         42  
  18         1544  
93 18     18   106 use constant DEFAULT_CLI_DRIVER_FILE => 'cli-driver.yml';
  18         40  
  18         2267  
94              
95             ##############################################################################
96             ### ATTRIBUTES
97             ##############################################################################
98              
99             =head1 ATTRIBUTES
100              
101             =head2 path
102              
103             Directory where your cli-driver.yml file is located. You can specify
104             multiple directories by separating them with ':'. For example,
105             "etc:/etc".
106              
107             isa: Str
108              
109             defaults: .:etc:/etc
110              
111             =cut
112              
113             has path => (
114             is => 'rw',
115             isa => 'Str',
116             );
117              
118             =head2 file
119              
120             Name of your YAML driver file.
121              
122             isa: Str
123              
124             default: cli-driver.yml
125              
126             =cut
127              
128             has file => (
129             is => 'ro',
130             isa => 'Str',
131             lazy => 1,
132             builder => '_build_file'
133             );
134              
135             =head2 use_file_sharedir
136              
137             Flag indicating you want to use File::ShareDir to locate the driver file.
138             Requires the attribute 'file_sharedir_dist_name' to be provided. Is mutually
139             exclusive with the 'path' attribute.
140              
141             isa: Bool
142              
143             default: 0
144              
145             =cut
146              
147             has use_file_sharedir => (
148             is => 'ro',
149             isa => 'Bool',
150             default => 0,
151             );
152              
153             =head2 file_sharedir_dist_name
154              
155             Your distro name. For example: 'CLI-Driver'.
156              
157             isa: Str
158              
159             default: undef
160              
161             =cut
162              
163             has file_sharedir_dist_name => (
164             is => 'ro',
165             isa => 'Str',
166             );
167              
168             =head2 argv_map
169              
170             A set of command line overrides for retrieving arguments. This can be used
171             in-place of @ARGV args.
172              
173             Example:
174              
175             {
176             classAttrName1 => 'abc',
177             classAttrName2 => 'def',
178             methodArgName1 => 'ghi'
179             }
180              
181             isa: HashRef
182              
183             default: undef
184              
185             =cut
186              
187             # notice the cli switches are not part of the map.
188             has argv_map => (
189             is => 'rw',
190             isa => 'HashRef',
191             predicate => 'has_argv_map',
192             writer => '_set_argv_map',
193             );
194              
195             =head2 actions
196              
197             A list of actions parsed from the driver file.
198              
199             isa: ArrayRef[CLI::Driver::Action]
200              
201             =cut
202              
203             has actions => (
204             is => 'rw',
205             isa => 'ArrayRef[CLI::Driver::Action]',
206             lazy => 1,
207             builder => '_build_actions',
208             );
209              
210             ##############################################################################
211             ### PUBLIC METHODS
212             ##############################################################################
213              
214 18 50   18   35652 method BUILD (@argv) {
  18 50   17   45  
  18         2735  
  17         79  
  17         149  
  17         39  
215              
216 17 100       621 if ( $self->has_argv_map ) {
217 1         24 $self->_build_global_argv_map( $self->argv_map );
218             }
219             }
220              
221 18 0   18   41092 method set_argv_map (HashRef $argv_map) {
  18 0   18   59  
  18 0   0   3035  
  18 0       126  
  18 0       63  
  18         1814  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
222              
223 0         0 $self->_set_argv_map( {%$argv_map} );
224 0         0 $self->_build_global_argv_map( $self->argv_map );
225             }
226              
227 18 50 33 18   38258 method get_action (Str :$name!) {
  18 50 33 18   43  
  18 50   18   2019  
  18 50   18   147  
  18 50   18   55  
  18 50   21   1026  
  18         133  
  18         40  
  18         171  
  18         1792  
  18         47  
  18         6351  
  18         130  
  18         39  
  18         3620  
  21         13557  
  21         75  
  21         43  
  21         123  
  0         0  
  21         52  
  21         81  
  21         93  
  0         0  
  21         84  
  21         75  
  21         93  
  21         73  
  21         44  
  21         102  
  21         186  
  21         107  
  21         79  
  21         38  
  21         128  
  21         42  
228              
229 21         108 my $actions = $self->get_actions;
230              
231 21         71 foreach my $action (@$actions) {
232 155 100       3491 if ( $action->name eq $name ) {
233 21         163 return $action;
234             }
235             }
236             }
237              
238 18 50 33 18   48195 method get_actions (Bool :$want_hashref = 0) {
  18 50 0 18   53  
  18 50   18   1947  
  18 50   18   141  
  18 50   18   36  
  18     21   836  
  18         122  
  18         40  
  18         120  
  18         1414  
  18         44  
  18         6196  
  18         127  
  18         42  
  18         4709  
  21         88  
  21         46  
  21         44  
  21         115  
  0         0  
  21         52  
  21         58  
  21         89  
  0         0  
  21         109  
  0         0  
  0         0  
  0         0  
  21         48  
  21         87  
  21         91  
  21         188  
  21         46  
  21         134  
  21         45  
239              
240 21         48 my @ret = @{ $self->actions };
  21         694  
241              
242 21 50       77 if ($want_hashref) {
243              
244 0         0 my %actions;
245 0         0 foreach my $action (@ret) {
246 0         0 my $name = $action->name;
247 0 0       0 next if $name =~ /dummy/i;
248 0         0 $actions{$name} = $action;
249             }
250              
251 0         0 return \%actions;
252             }
253              
254 21         97 return \@ret;
255             }
256              
257 18 0   18   18998 method run {
  18     0   54  
  18         2326  
  0         0  
  0         0  
258              
259 0         0 my $action = $self->parse_cmd_line();
260 0 0       0 if ($action) {
261 0         0 $action->do;
262             }
263             else {
264 0         0 $self->fatal("failed to find action in config file");
265             }
266             }
267              
268 18 0   18   18988 method parse_cmd_line {
  18     0   53  
  18         6317  
  0         0  
  0         0  
269              
270 0         0 my $help;
271             my $action_name;
272 0         0 my $dump;
273              
274 0         0 GetOptions( #
275             "dump" => \$dump,
276             "help|?" => \$help
277             );
278              
279 0 0       0 if ( !@ARGV ) {
    0          
280 0         0 $self->usage;
281             }
282             elsif (@ARGV) {
283 0         0 $action_name = shift @ARGV;
284             }
285              
286 0         0 my $action;
287 0 0       0 if ($action_name) {
288 0         0 $action = $self->get_action( name => $action_name );
289            
290 0 0       0 if ($dump) {
291 0         0 say $action->to_yaml;
292 0         0 exit;
293             }
294             }
295              
296 0 0       0 if ($help) {
297 0 0       0 if ($action) {
298 0         0 $action->usage;
299             }
300             else {
301 0         0 $self->usage;
302             }
303             }
304              
305 0         0 return $action;
306             }
307              
308 18 0   18   43596 method usage (Str $errmsg?) {
  18 0   18   62  
  18 0   0   3009  
  18 0       136  
  18 0       46  
  18         6537  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
309              
310 0 0       0 print STDERR "$errmsg\n" if $errmsg;
311 0         0 print "\nusage: $0 <action> [opts] [-?] [--dump]\n\n";
312              
313 0         0 my @list;
314 0         0 my $actions = $self->get_actions;
315              
316 0         0 foreach my $action (@$actions) {
317              
318 0 0       0 next if $action->name =~ /dummy/i;
319              
320 0         0 my @display;
321 0         0 push @display, $action->name;
322              
323 0 0       0 if ( $action->is_deprecated ) {
324 0         0 my $depr = $action->deprecated;
325 0         0 push @display, sprintf '(%s)', $depr->get_usage_modifier;
326             }
327              
328 0         0 push @list, join( ' ', @display );
329             }
330              
331 0         0 say "\tACTIONS:";
332              
333 0         0 foreach my $action ( sort @list ) {
334 0         0 print "\t\t$action\n";
335             }
336              
337 0         0 print "\n";
338 0         0 exit 1;
339             }
340              
341             ##############################################################################
342             ### PRIVATE METHODS
343             ##############################################################################
344              
345 18 50   18   20003 method _find_file {
  18     17   42  
  18         8010  
  17         81  
  17         42  
346              
347 17         32 my @search_dirs;
348              
349 17 50       507 if ( $self->use_file_sharedir ) {
350              
351 0         0 my $dist_name = $self->file_sharedir_dist_name;
352 0 0       0 if ( !$dist_name ) {
353 0         0 confess "must provide file_sharedir_dist_name "
354             . "when use_file_sharedir is true";
355             }
356              
357 0         0 load 'File::ShareDir';
358              
359 0         0 @search_dirs = ('./share');
360 0         0 push @search_dirs, File::ShareDir::dist_dir($dist_name);
361             }
362             else {
363              
364 17 50       456 if ( $self->path ) {
365 17         393 push @search_dirs, split( /:/, $self->path );
366             }
367              
368 17         105 push @search_dirs, DEFAULT_CLI_DRIVER_PATH;
369             }
370              
371 17         75 foreach my $path (@search_dirs) {
372 17         441 my $fullpath = sprintf "%s/%s", $path, $self->file;
373 17 50       401 if ( -f $fullpath ) {
374 17         112 return $fullpath;
375             }
376             }
377              
378 0         0 my $msg = sprintf "unable to find %s in: %s", $self->file,
379             join( ', ', @search_dirs );
380 0         0 confess $msg;
381             }
382              
383 18 50   18   19299 method _build_actions {
  18     17   46  
  18         4599  
  17         96  
  17         37  
384              
385 17         36 my @actions;
386              
387 17         92 my $driver_file = $self->_find_file;
388 17         110 my $actions = $self->_parse_yaml( path => $driver_file );
389              
390 17         100 foreach my $action_name ( keys %$actions ) {
391              
392             my $action = CLI::Driver::Action->new(
393 272 100       7863 href => $actions->{$action_name},
394             name => $action_name,
395             use_argv_map => $self->has_argv_map ? 1 : 0
396             );
397              
398 272         890 my $success = $action->parse;
399 272 50       563 if ($success) {
400 272         709 push @actions, $action;
401             }
402             }
403              
404 17         549 return \@actions;
405             }
406              
407 18 50 33 18   38748 method _parse_yaml (Str :$path!) {
  18 50 33 18   60  
  18 50   18   1954  
  18 50   18   122  
  18 50   18   43  
  18 50   17   964  
  18         125  
  18         40  
  18         164  
  18         1609  
  18         53  
  18         6325  
  18         125  
  18         41  
  18         3102  
  17         79  
  17         42  
  17         39  
  17         117  
  0         0  
  17         43  
  17         50  
  17         74  
  0         0  
  17         66  
  17         50  
  17         78  
  17         61  
  17         33  
  17         69  
  17         133  
  17         98  
  17         74  
  17         43  
  17         88  
  17         42  
408              
409 17         39 my $href;
410 17         35 eval {
411 17         119 $href = YAML::Syck::LoadFile($path);
412             };
413 17 50       17655 confess $@ if $@;
414              
415 17         80 return $href;
416             }
417              
418 18 0   18   19822 method _build_file {
  18     0   46  
  18         2786  
  0         0  
  0         0  
419              
420 0 0       0 if ( $ENV{CLI_DRIVER_FILE} ) {
421 0         0 return $ENV{CLI_DRIVER_FILE};
422             }
423              
424 0         0 return DEFAULT_CLI_DRIVER_FILE;
425             }
426              
427 18 50   18   38530 method _build_global_argv_map (HashRef $argv_map) {
  18 50   18   46  
  18 50   1   2910  
  18 50       131  
  18 50       40  
  18         3435  
  1         4  
  1         4  
  1         5  
  1         5  
  1         1  
  1         5  
  1         2  
428              
429 1         3 %ARGV = ();
430              
431 1         5 foreach my $key ( keys %$argv_map ) {
432 4         27 $ARGV{$key} = $argv_map->{$key};
433             }
434             }
435              
436             __PACKAGE__->meta->make_immutable;
437              
438             1;