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   861932 use Modern::Perl;
  18         9872  
  18         133  
10 18     18   13144 use Moose;
  18         8698803  
  18         127  
11 18     18   147777 use namespace::autoclean;
  18         153256  
  18         73  
12 18     18   10616 use Kavorka 'method';
  18         193922  
  18         167  
13 18     18   3614586 use Data::Printer alias => 'pdump';
  18         37553  
  18         199  
14 18     18   12308 use CLI::Driver::Action;
  18         84  
  18         993  
15 18     18   173 use Module::Load;
  18         44  
  18         177  
16              
17 18     18   1300 use Getopt::Long;
  18         40  
  18         186  
18             Getopt::Long::Configure('no_ignore_case');
19             Getopt::Long::Configure('pass_through');
20             Getopt::Long::Configure('no_auto_abbrev');
21              
22 18     18   3814 use YAML::Syck;
  18         48  
  18         1928  
23             $YAML::Syck::ImplicitTyping = 1;
24              
25             with 'CLI::Driver::CommonRole';
26              
27             our $VERSION = 0.75;
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   126 use constant DEFAULT_CLI_DRIVER_PATH => ( '.', 'etc', '/etc' );
  18         44  
  18         1523  
93 18     18   124 use constant DEFAULT_CLI_DRIVER_FILE => 'cli-driver.yml';
  18         40  
  18         2359  
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   37901 method BUILD (@argv) {
  18 50   17   52  
  18         2932  
  17         82  
  17         178  
  17         40  
215              
216 17 100       628 if ( $self->has_argv_map ) {
217 1         27 $self->_build_global_argv_map( $self->argv_map );
218             }
219             }
220              
221 18 0   18   45888 method set_argv_map (HashRef $argv_map) {
  18 0   18   63  
  18 0   0   3295  
  18 0       139  
  18 0       41  
  18         2079  
  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   42787 method get_action (Str :$name!) {
  18 50 33 18   48  
  18 50   18   2585  
  18 50   18   221  
  18 50   18   57  
  18 50   21   1084  
  18         118  
  18         38  
  18         217  
  18         1987  
  18         86  
  18         6847  
  18         141  
  18         41  
  18         3858  
  21         10297  
  21         59  
  21         46  
  21         121  
  0         0  
  21         51  
  21         72  
  21         88  
  0         0  
  21         88  
  21         62  
  21         112  
  21         79  
  21         44  
  21         93  
  21         177  
  21         110  
  21         85  
  21         45  
  21         144  
  21         40  
228              
229 21         157 my $actions = $self->get_actions;
230              
231 21         77 foreach my $action (@$actions) {
232 178 100       4352 if ( $action->name eq $name ) {
233 21         176 return $action;
234             }
235             }
236             }
237              
238 18 50 33 18   53216 method get_actions (Bool :$want_hashref = 0) {
  18 50 0 18   63  
  18 50   18   2241  
  18 50   18   137  
  18 50   18   37  
  18     21   852  
  18         130  
  18         59  
  18         125  
  18         1647  
  18         63  
  18         6760  
  18         136  
  18         50  
  18         4954  
  21         87  
  21         51  
  21         44  
  21         110  
  0         0  
  21         50  
  21         54  
  21         75  
  0         0  
  21         95  
  0         0  
  0         0  
  0         0  
  21         43  
  21         91  
  21         102  
  21         182  
  21         46  
  21         128  
  21         45  
239              
240 21         57 my @ret = @{ $self->actions };
  21         691  
241              
242 21 50       109 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         101 return \@ret;
255             }
256              
257 18 0   18   21014 method run {
  18     0   48  
  18         2547  
  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   21539 method parse_cmd_line {
  18     0   59  
  18         6410  
  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   47404 method usage (Str $errmsg?) {
  18 0   18   62  
  18 0   0   3464  
  18 0       144  
  18 0       41  
  18         7544  
  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] [-?]\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   21991 method _find_file {
  18     17   49  
  18         8588  
  17         67  
  17         48  
346              
347 17         38 my @search_dirs;
348              
349 17 50       572 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       504 if ( $self->path ) {
365 17         449 push @search_dirs, split( /:/, $self->path );
366             }
367              
368 17         97 push @search_dirs, DEFAULT_CLI_DRIVER_PATH;
369             }
370              
371 17         55 foreach my $path (@search_dirs) {
372 17         455 my $fullpath = sprintf "%s/%s", $path, $self->file;
373 17 50       486 if ( -f $fullpath ) {
374 17         124 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   21044 method _build_actions {
  18     17   48  
  18         5022  
  17         122  
  17         41  
384              
385 17         45 my @actions;
386              
387 17         89 my $driver_file = $self->_find_file;
388 17         125 my $actions = $self->_parse_yaml( path => $driver_file );
389              
390 17         134 foreach my $action_name ( keys %$actions ) {
391              
392             my $action = CLI::Driver::Action->new(
393 272 100       8486 href => $actions->{$action_name},
394             name => $action_name,
395             use_argv_map => $self->has_argv_map ? 1 : 0
396             );
397              
398 272         1023 my $success = $action->parse;
399 272 50       575 if ($success) {
400 272         834 push @actions, $action;
401             }
402             }
403              
404 17         533 return \@actions;
405             }
406              
407 18 50 33 18   43135 method _parse_yaml (Str :$path!) {
  18 50 33 18   69  
  18 50   18   2286  
  18 50   18   144  
  18 50   18   44  
  18 50   17   924  
  18         126  
  18         48  
  18         162  
  18         1745  
  18         70  
  18         7102  
  18         135  
  18         39  
  18         3487  
  17         92  
  17         52  
  17         40  
  17         102  
  0         0  
  17         49  
  17         52  
  17         79  
  0         0  
  17         76  
  17         69  
  17         75  
  17         63  
  17         37  
  17         75  
  17         144  
  17         89  
  17         100  
  17         38  
  17         102  
  17         39  
408              
409 17         34 my $href;
410 17         38 eval {
411 17         112 $href = YAML::Syck::LoadFile($path);
412             };
413 17 50       19407 confess $@ if $@;
414              
415 17         84 return $href;
416             }
417              
418 18 0   18   21954 method _build_file {
  18     0   43  
  18         2685  
  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   42761 method _build_global_argv_map (HashRef $argv_map) {
  18 50   18   81  
  18 50   1   3481  
  18 50       141  
  18 50       42  
  18         4210  
  1         4  
  1         4  
  1         4  
  1         5  
  1         2  
  1         6  
  1         2  
428              
429 1         3 %ARGV = ();
430              
431 1         5 foreach my $key ( keys %$argv_map ) {
432 4         32 $ARGV{$key} = $argv_map->{$key};
433             }
434             }
435              
436             __PACKAGE__->meta->make_immutable;
437              
438             1;