File Coverage

blib/lib/CLI/Osprey/Role.pm
Criterion Covered Total %
statement 137 195 70.2
branch 54 106 50.9
condition 20 56 35.7
subroutine 13 17 76.4
pod 0 5 0.0
total 224 379 59.1


line stmt bran cond sub pod time code
1             package CLI::Osprey::Role;
2 4     4   2831 use strict;
  4         9  
  4         158  
3 4     4   17 use warnings;
  4         8  
  4         223  
4 4     4   20 use Carp 'croak';
  4         6  
  4         276  
5 4     4   3693 use Path::Tiny ();
  4         69316  
  4         192  
6 4     4   41 use Scalar::Util qw(blessed);
  4         6  
  4         376  
7 4     4   24 use Module::Runtime 'use_module';
  4         23  
  4         40  
8              
9 4     4   2693 use CLI::Osprey::Descriptive;
  4         14  
  4         40  
10              
11             # ABSTRACT: Role for CLI::Osprey applications
12             our $VERSION = '0.09'; # VERSION
13             our $AUTHORITY = 'cpan:ARODLAND'; # AUTHORITY
14              
15             sub _osprey_option_to_getopt {
16 91     91   37464 my ($name, %attributes) = @_;
17              
18             # Use custom option name if provided, otherwise use attribute name
19 91   33     214 my $option_name = $attributes{option} || $name;
20              
21 91         266 my $getopt = join('|', grep defined, ($option_name, $attributes{short}));
22 91 50 33     194 $getopt .= '+' if $attributes{repeatable} && !defined $attributes{format};
23 91 100       187 $getopt .= '!' if $attributes{negatable};
24 91 100       216 $getopt .= '=' . $attributes{format} if defined $attributes{format};
25 91 50 33     148 $getopt .= '@' if $attributes{repeatable} && defined $attributes{format};
26              
27 91         329 return $getopt;
28             }
29              
30             sub _osprey_prepare_options {
31 40     40   69 my ($options, $config) = @_;
32              
33 40         106 my @getopt;
34             my %abbreviations;
35 40         0 my %fullnames;
36              
37             # If options have an 'order' attr, use it; those without sort as though they were 9,999.
38             # If the order is equal (or not present on both) and the 'added_order' config flag is set,
39             # then sort according to added_order.
40             # Otherwise, sort according to option name.
41             my @order = sort {
42 40         149 ($options->{$a}{order} || 9999) <=> ($options->{$b}{order} || 9999)
43 107 50 50     511 || ($config->{added_order} ? ($options->{$a}{added_order} <=> $options->{$b}{added_order}) : 0)
    0 50        
      33        
44             || $a cmp $b
45             } keys %$options;
46              
47 40         124 for my $option (@order) {
48 87         93 my %attributes = %{ $options->{$option} };
  87         339  
49              
50 87         126 push @{ $fullnames{ $attributes{option} } }, $option;
  87         254  
51             }
52              
53 40         91 for my $name (keys %fullnames) {
54 87 50       86 if (@{ $fullnames{$name} } > 1) {
  87         174  
55 0         0 croak "Multiple option attributes named $name: [@{ $fullnames{$name} }]";
  0         0  
56             }
57             }
58              
59 40         104 for my $option (@order) {
60 87         98 my %attributes = %{ $options->{$option} };
  87         248  
61              
62 87         130 my $name = $attributes{option};
63 87         111 my $doc = $attributes{doc};
64 87 50       137 $doc = "no documentation for $name" unless defined $doc;
65              
66 87 50       134 push @getopt, [] if $attributes{spacer_before};
67 87 50       612 push @getopt, [ _osprey_option_to_getopt($option, %attributes), $doc, ($attributes{hidden} ? { hidden => 1} : ()) ];
68 87 50       139 push @getopt, [] if $attributes{spacer_after};
69              
70 87         88 push @{ $abbreviations{$name} }, $option;
  87         149  
71              
72             # If we allow abbreviating long option names, an option can be called by any prefix of its name,
73             # unless that prefix is an option name itself. Ambiguous cases (an abbreviation is a prefix of
74             # multiple option names) are handled later in _osprey_fix_argv.
75 87 50       142 if ($config->{abbreviate}) {
76 87         160 for my $len (1 .. length($name) - 1) {
77 844         964 my $abbreviated = substr $name, 0, $len;
78 844 50       1092 push @{ $abbreviations{$abbreviated} }, $option unless exists $fullnames{$abbreviated};
  844         1968  
79             }
80             }
81             }
82              
83 40         183 return \@getopt, \%abbreviations;
84             }
85              
86             # Process ARGV, rewriting options to be what GLD expects them to be.
87             # We only want to rewrite things that GLD will be happy about, because it would be
88             # bad to have GLD generate an error about something being invalid, if it isn't
89             # actually something that the user typed!
90             # Stuff that's done here:
91             # * Rewrite abbreviations to their equivalent full names.
92             # * Rewrite options that were aliased with 'option' or the default underscore-to-dash
93             # rewriting, to their canonical name (the same as the attribute name).
94             # * Recognize '--foo=bar' options and replace them with '--foo bar'.
95             sub _osprey_fix_argv {
96 40     40   105 my ($options, $abbreviations) = @_;
97              
98 40         72 my @new_argv;
99              
100 40         155 while (defined( my $arg = shift @ARGV )) {
101             # As soon as we find a -- or a non-option word, stop processing and leave everything
102             # from there onwards in ARGV as either positional args or a subcommand.
103 38 100 33     270 if ($arg eq '--' or $arg eq '-' or $arg !~ /^-/) {
      66        
104 22         59 push @new_argv, $arg, @ARGV;
105 22         44 last;
106             }
107              
108 16         55 my ($arg_name_with_dash, $arg_value) = split /=/, $arg, 2;
109 16 50       51 unshift @ARGV, $arg_value if defined $arg_value;
110              
111 16         117 my ($dash, $negative, $arg_name_without_dash)
112             = $arg_name_with_dash =~ /^(-+)(no\-)?(.+)$/;
113              
114 16         22 my $option_name;
115            
116             # If this is a long option and abbreviations are enabled, search the abbreviation table
117             # for options that match the prefix. If the result is unique, use it. If the result is
118             # ambiguous, set $option_name undef so that eventually we will generate an "unknown option"
119             # error for it. Prefixes that exactly match an option name are excluded from the table, so
120             # that assuming that 'foo' and 'foobar' are both options, '--fo' will be ambiguous,
121             # '--foo' will be unambiguously 'foo' (*not* an error even though there are in fact two
122             # optiions that start with 'foo'), and '--foob' will be 'foobar'.
123 16 100       35 if ($dash eq '--') {
124 11         17 my $option_names = $abbreviations->{$arg_name_without_dash};
125 11 50       24 if (defined $option_names) {
126 11 50       23 if (@$option_names == 1) {
127 11         20 $option_name = $option_names->[0];
128             } else {
129             # TODO: can't we produce a warning saying that it's ambiguous and which options conflict?
130 0         0 $option_name = undef;
131             }
132             }
133             }
134              
135             # $option_name is the attribute name (underscored) from abbreviations table
136             # We need to get the actual CLI option name for the rewritten ARGV
137 16   50     67 my $arg_name = ($dash || '') . ($negative || '');
      100        
138 16 100       29 if (defined $option_name) {
139             # Use the custom option name from the options hash if possible
140 11   33     30 $arg_name .= $options->{$option_name}{option} || $option_name;
141             } else {
142 5         10 $arg_name .= $arg_name_without_dash;
143             }
144              
145 16         23 push @new_argv, $arg_name;
146 16 100 100     58 if (defined $option_name && $options->{$option_name}{format}) {
147 9         25 push @new_argv, shift @ARGV;
148             }
149             }
150              
151 40         116 return @new_argv;
152             }
153              
154 4     4   6272 use Moo::Role;
  4         7  
  4         41  
155              
156             requires qw(_osprey_config _osprey_options _osprey_subcommands);
157              
158             has 'parent_command' => (
159             is => 'ro',
160             );
161              
162             has 'invoked_as' => (
163             is => 'ro',
164             );
165              
166             sub new_with_options {
167 39     39 0 830777 my ($class, %params) = @_;
168 39         1125 my %config = $class->_osprey_config;
169              
170 39 50       805 local @ARGV = @ARGV if $config{protect_argv};
171              
172 39 100       112 if (!defined $params{invoked_as}) {
173 23         123 $params{invoked_as} = Getopt::Long::Descriptive::prog_name();
174             }
175              
176 39         247 my ($parsed_params, $usage) = $class->parse_options(%params);
177              
178 39 50       184 if ($parsed_params->{h}) {
    50          
    50          
179 0         0 return $class->osprey_usage(1, $usage);
180             } elsif ($parsed_params->{help}) {
181 0         0 return $class->osprey_help(1, $usage);
182             } elsif ($parsed_params->{man}) {
183 0         0 return $class->osprey_man($usage);
184             }
185              
186 39         56 my %merged_params;
187 39 50       88 if ($config{prefer_commandline}) {
188 39         149 %merged_params = (%params, %$parsed_params);
189             } else {
190 0         0 %merged_params = (%$parsed_params, %params);
191             }
192              
193 39         1335 my %subcommands = $class->_osprey_subcommands;
194 39         502 my ($subcommand_name, $subcommand_class);
195 39 100 66     149 if (@ARGV && $ARGV[0] ne '--') { # Check what to do with remaining options
196 17 50       160 if ($ARGV[0] =~ /^--/) { # Getopt stopped at an unrecognized option, error.
    50          
197 0         0 print STDERR "Unknown option '$ARGV[0]'.\n";
198 0         0 return $class->osprey_usage(1, $usage);
199             } elsif (%subcommands) {
200 17         31 $subcommand_name = shift @ARGV; # Remove it so the subcommand sees only options
201 17         37 $subcommand_class = $subcommands{$subcommand_name};
202 17 50       50 if (!defined $subcommand_class) {
203 0         0 print STDERR "Unknown subcommand '$subcommand_name'.\n";
204 0         0 return $class->osprey_usage(1, $usage);
205             }
206             }
207             # If we're not expecting a subcommand, and getopt didn't stop at an option, consider the remainder
208             # as positional args and leave them in ARGV.
209             }
210              
211 39         54 my $self;
212 39 50       56 unless (eval { $self = $class->new(%merged_params); 1 }) {
  39         612  
  39         10595  
213 0 0       0 if ($@ =~ /^Attribute \((.*?)\) is required/) {
    0          
    0          
    0          
214 0         0 print STDERR "$1 is missing\n";
215             } elsif ($@ =~ /^Missing required arguments: (.*) at /) {
216 0         0 my @missing_required = split /,\s/, $1;
217 0         0 print STDERR "$_ is missing\n" for @missing_required;
218             } elsif ($@ =~ /^(.*?) required/) {
219 0         0 print STDERR "$1 is missing\n";
220             } elsif ($@ =~ /^isa check .*?failed: /) {
221 0         0 print STDERR substr($@, index($@, ':') + 2);
222             } else {
223 0         0 print STDERR $@;
224             }
225 0         0 return $class->osprey_usage(1, $usage);
226             }
227              
228 39 100       499 return $self unless $subcommand_class;
229              
230 17 100       100 use_module($subcommand_class) unless ref $subcommand_class;
231              
232 17         706 return $subcommand_class->new_with_options(
233             %params,
234             parent_command => $self,
235             invoked_as => "$params{invoked_as} $subcommand_name"
236             );
237             }
238              
239             sub parse_options {
240 40     40 0 158336 my ($class, %params) = @_;
241              
242 40         820 my %options = $class->_osprey_options;
243 40         1251 my %config = $class->_osprey_config;
244 40         1077 my %subcommands = $class->_osprey_subcommands;
245              
246 40         491 my ($options, $abbreviations) = _osprey_prepare_options(\%options, \%config);
247 40         150 @ARGV = _osprey_fix_argv(\%options, $abbreviations);
248              
249 40 100       124 my @getopt_options = %subcommands ? qw(require_order) : ();
250              
251 40 50       122 push @getopt_options, @{$config{getopt_options}} if defined $config{getopt_options};
  0         0  
252              
253 40         85 my $prog_name = $params{invoked_as};
254 40 100       89 $prog_name = Getopt::Long::Descriptive::prog_name() if !defined $prog_name;
255              
256 40         78 my $usage_str = $config{usage_string};
257 40 50       105 unless (defined $usage_str) {
258 40 100       99 if (%subcommands) {
259 23         81 $usage_str = "Usage: $prog_name %o [subcommand]";
260             } else {
261 17         35 $usage_str = "Usage: $prog_name %o";
262             }
263             }
264              
265 40         313 my ($opt, $usage) = describe_options(
266             $usage_str,
267             @$options,
268             [],
269             [ 'h', "show a short help message" ],
270             [ 'help', "show a long help message" ],
271             [ 'man', "show the manual" ],
272             { getopt_conf => \@getopt_options },
273             );
274              
275 40         43915 $usage->{prog_name} = $prog_name;
276 40         84 $usage->{target} = $class;
277              
278 40 50       116 if ($usage->{should_die}) {
279 0         0 return $class->osprey_usage(1, $usage);
280             }
281              
282 40         57 my %parsed_params;
283              
284 40         116 for my $name (keys %options, qw(h help man)) {
285             # Getopt::Long converts hyphens to underscores; Getopt::Long::Descriptive uses these as method names
286             # For custom option names, we need to retrieve using the option name, not attribute name
287 207         239 my $method_name = $name;
288 207 50 66     441 if (exists $options{$name} && exists $options{$name}{option}) {
289 87         111 $method_name = $options{$name}{option};
290 87         158 $method_name =~ tr/-/_/; # Convert hyphens to underscores to match Getopt::Long's conversion
291             }
292              
293 207         423 my $val = $opt->$method_name();
294 207 100       793 $parsed_params{$name} = $val if defined $val;
295             }
296              
297 40         499 return \%parsed_params, $usage;
298              
299             }
300              
301             sub osprey_usage {
302 0     0 0   my ($class, $code, @messages) = @_;
303              
304 0           my $usage;
305              
306 0 0 0       if (@messages && blessed($messages[0]) && $messages[0]->isa('CLI::Osprey::Descriptive::Usage')) {
      0        
307 0           $usage = shift @messages;
308             } else {
309 0           local @ARGV = ();
310 0           (undef, $usage) = $class->parse_options(help => 1);
311             }
312              
313 0           my $message;
314 0 0         $message = join("\n", @messages, '') if @messages;
315 0           $message .= $usage . "\n";
316              
317 0 0         if ($code) {
318 0           CORE::warn $message;
319             } else {
320 0           print $message;
321             }
322 0 0         exit $code if defined $code;
323 0           return;
324             }
325              
326             sub osprey_help {
327 0     0 0   my ($class, $code, $usage) = @_;
328              
329 0 0 0       unless (defined $usage && blessed($usage) && $usage->isa('CLI::Osprey::Descriptive::Usage')) {
      0        
330 0           local @ARGV = ();
331 0           (undef, $usage) = $class->parse_options(help => 1);
332             }
333              
334 0           my $message = $usage->option_help . "\n";
335              
336 0 0         if ($code) {
337 0           CORE::warn $message;
338             } else {
339 0           print $message;
340             }
341 0 0         exit $code if defined $code;
342 0           return;
343             }
344              
345             sub osprey_man {
346 0     0 0   my ($class, $usage, $output) = @_;
347              
348 0 0 0       unless (defined $usage && blessed($usage) && $usage->isa('CLI::Osprey::Descriptive::Usage')) {
      0        
349 0           local @ARGV = ();
350 0           (undef, $usage) = $class->parse_options(man => 1);
351             }
352              
353 0           my $tmpdir = Path::Tiny->tempdir;
354 0           my $podfile = $tmpdir->child("help.pod");
355 0           $podfile->spew_utf8($usage->option_pod);
356              
357 0           require Pod::Usage;
358 0           Pod::Usage::pod2usage(
359             -verbose => 2,
360             -input => "$podfile",
361             -exitval => 'NOEXIT',
362             -output => $output,
363             );
364              
365 0           exit(0);
366             }
367              
368             sub _osprey_subcommand_desc {
369 0     0     my ($class) = @_;
370 0           my %config = $class->_osprey_config;
371 0           return $config{desc};
372             }
373              
374             1;
375              
376             __END__