File Coverage

blib/lib/CLI/Osprey/Role.pm
Criterion Covered Total %
statement 130 190 68.4
branch 47 104 45.1
condition 10 47 21.2
subroutine 13 17 76.4
pod 0 5 0.0
total 200 363 55.1


line stmt bran cond sub pod time code
1             package CLI::Osprey::Role;
2 4     4   2429 use strict;
  4         7  
  4         120  
3 4     4   21 use warnings;
  4         7  
  4         118  
4 4     4   26 use Carp 'croak';
  4         18  
  4         246  
5 4     4   3387 use Path::Tiny ();
  4         47033  
  4         168  
6 4     4   39 use Scalar::Util qw(blessed);
  4         8  
  4         248  
7 4     4   33 use Module::Runtime 'use_module';
  4         13  
  4         35  
8              
9 4     4   2154 use CLI::Osprey::Descriptive;
  4         14  
  4         47  
10              
11             # ABSTRACT: Role for CLI::Osprey applications
12             our $VERSION = '0.08'; # VERSION
13             our $AUTHORITY = 'cpan:ARODLAND'; # AUTHORITY
14              
15             sub _osprey_option_to_getopt {
16 7     7   29 my ($name, %attributes) = @_;
17 7         33 my $getopt = join('|', grep defined, ($name, $attributes{short}));
18 7 50 33     23 $getopt .= '+' if $attributes{repeatable} && !defined $attributes{format};
19 7 50       16 $getopt .= '!' if $attributes{negatable};
20 7 50       21 $getopt .= '=' . $attributes{format} if defined $attributes{format};
21 7 50 33     25 $getopt .= '@' if $attributes{repeatable} && defined $attributes{format};
22 7         38 return $getopt;
23             }
24              
25             sub _osprey_prepare_options {
26 16     16   42 my ($options, $config) = @_;
27              
28 16         41 my @getopt;
29             my %abbreviations;
30 16         0 my %fullnames;
31              
32             my @order = sort {
33 16         53 ($options->{$a}{order} || 9999) <=> ($options->{$b}{order} || 9999)
34 0 0 0     0 || ($config->{added_order} ? ($options->{$a}{added_order} <=> $options->{$b}{added_order}) : 0)
    0 0        
      0        
35             || $a cmp $b
36             } keys %$options;
37              
38 16         40 for my $option (@order) {
39 7         11 my %attributes = %{ $options->{$option} };
  7         29  
40              
41 7         15 push @{ $fullnames{ $attributes{option} } }, $option;
  7         30  
42             }
43              
44 16         48 for my $name (keys %fullnames) {
45 7 50       11 if (@{ $fullnames{$name} } > 1) {
  7         23  
46 0         0 croak "Multiple option attributes named $name: [@{ $fullnames{$name} }]";
  0         0  
47             }
48             }
49              
50 16         32 for my $option (@order) {
51 7         9 my %attributes = %{ $options->{$option} };
  7         22  
52              
53 7         13 my $name = $attributes{option};
54 7         13 my $doc = $attributes{doc};
55 7 50       17 $doc = "no documentation for $name" unless defined $doc;
56              
57 7 50       16 push @getopt, [] if $attributes{spacer_before};
58 7 50       24 push @getopt, [ _osprey_option_to_getopt($option, %attributes), $doc, ($attributes{hidden} ? { hidden => 1} : ()) ];
59 7 50       18 push @getopt, [] if $attributes{spacer_after};
60              
61 7         9 push @{ $abbreviations{$name} }, $option;
  7         20  
62              
63             # If we allow abbreviating long option names, an option can be called by any prefix of its name,
64             # unless that prefix is an option name itself. Ambiguous cases (an abbreviation is a prefix of
65             # multiple option names) are handled later in _osprey_fix_argv.
66 7 50       15 if ($config->{abbreviate}) {
67 7         21 for my $len (1 .. length($name) - 1) {
68 60         99 my $abbreviated = substr $name, 0, $len;
69 60 50       98 push @{ $abbreviations{$abbreviated} }, $name unless exists $fullnames{$abbreviated};
  60         160  
70             }
71             }
72             }
73              
74 16         56 return \@getopt, \%abbreviations;
75             }
76              
77             sub _osprey_fix_argv {
78 16     16   33 my ($options, $abbreviations) = @_;
79              
80 16         26 my @new_argv;
81              
82 16         53 while (defined( my $arg = shift @ARGV )) {
83             # As soon as we find a -- or a non-option word, stop processing and leave everything
84             # from there onwards in ARGV as either positional args or a subcommand.
85 7 100 33     71 if ($arg eq '--' or $arg eq '-' or $arg !~ /^-/) {
      66        
86 5         16 push @new_argv, $arg, @ARGV;
87 5         11 last;
88             }
89              
90 2         9 my ($arg_name_with_dash, $arg_value) = split /=/, $arg, 2;
91 2 50       6 unshift @ARGV, $arg_value if defined $arg_value;
92              
93 2         15 my ($dash, $negative, $arg_name_without_dash)
94             = $arg_name_with_dash =~ /^(-+)(no\-)?(.+)$/;
95              
96 2         4 my $option_name;
97            
98 2 50       7 if ($dash eq '--') {
99 2         6 my $option_names = $abbreviations->{$arg_name_without_dash};
100 2 50       6 if (defined $option_names) {
101 2 50       8 if (@$option_names == 1) {
102 2         6 $option_name = $option_names->[0];
103             } else {
104             # TODO: can't we produce a warning saying that it's ambiguous and which options conflict?
105 0         0 $option_name = undef;
106             }
107             }
108             }
109              
110 2   50     10 my $arg_name = ($dash || '') . ($negative || '');
      50        
111 2 50       6 if (defined $option_name) {
112 2         4 $arg_name .= $option_name;
113             } else {
114 0         0 $arg_name .= $arg_name_without_dash;
115             }
116              
117 2         4 push @new_argv, $arg_name;
118 2 50 33     10 if (defined $option_name && $options->{$option_name}{format}) {
119 2         7 push @new_argv, shift @ARGV;
120             }
121             }
122              
123 16         40 return @new_argv;
124             }
125              
126 4     4   4588 use Moo::Role;
  4         19  
  4         45  
127              
128             requires qw(_osprey_config _osprey_options _osprey_subcommands);
129              
130             has 'parent_command' => (
131             is => 'ro',
132             );
133              
134             has 'invoked_as' => (
135             is => 'ro',
136             );
137              
138             sub new_with_options {
139 15     15 0 46846 my ($class, %params) = @_;
140 15         423 my %config = $class->_osprey_config;
141              
142 15 50       439 local @ARGV = @ARGV if $config{protect_argv};
143              
144 15 100       61 if (!defined $params{invoked_as}) {
145 11         71 $params{invoked_as} = Getopt::Long::Descriptive::prog_name();
146             }
147              
148 15         119 my ($parsed_params, $usage) = $class->parse_options(%params);
149              
150 15 50       67 if ($parsed_params->{h}) {
    50          
    50          
151 0         0 return $class->osprey_usage(1, $usage);
152             } elsif ($parsed_params->{help}) {
153 0         0 return $class->osprey_help(1, $usage);
154             } elsif ($parsed_params->{man}) {
155 0         0 return $class->osprey_man($usage);
156             }
157              
158 15         23 my %merged_params;
159 15 50       32 if ($config{prefer_commandline}) {
160 15         55 %merged_params = (%params, %$parsed_params);
161             } else {
162 0         0 %merged_params = (%$parsed_params, %params);
163             }
164              
165 15         419 my %subcommands = $class->_osprey_subcommands;
166 15         192 my ($subcommand_name, $subcommand_class);
167 15 100 66     64 if (@ARGV && $ARGV[0] ne '--') { # Check what to do with remaining options
168 5 50       28 if ($ARGV[0] =~ /^--/) { # Getopt stopped at an unrecognized option, error.
    50          
169 0         0 print STDERR "Unknown option '$ARGV[0]'.\n";
170 0         0 return $class->osprey_usage(1, $usage);
171             } elsif (%subcommands) {
172 5         13 $subcommand_name = shift @ARGV; # Remove it so the subcommand sees only options
173 5         12 $subcommand_class = $subcommands{$subcommand_name};
174 5 50       18 if (!defined $subcommand_class) {
175 0         0 print STDERR "Unknown subcommand '$subcommand_name'.\n";
176 0         0 return $class->osprey_usage(1, $usage);
177             }
178             }
179             # If we're not expecting a subcommand, and getopt didn't stop at an option, consider the remainder
180             # as positional args and leave them in ARGV.
181             }
182              
183 15         25 my $self;
184 15 50       23 unless (eval { $self = $class->new(%merged_params); 1 }) {
  15         208  
  15         7617  
185 0 0       0 if ($@ =~ /^Attribute \((.*?)\) is required/) {
    0          
    0          
    0          
186 0         0 print STDERR "$1 is missing\n";
187             } elsif ($@ =~ /^Missing required arguments: (.*) at /) {
188 0         0 my @missing_required = split /,\s/, $1;
189 0         0 print STDERR "$_ is missing\n" for @missing_required;
190             } elsif ($@ =~ /^(.*?) required/) {
191 0         0 print STDERR "$1 is missing\n";
192             } elsif ($@ =~ /^isa check .*?failed: /) {
193 0         0 print STDERR substr($@, index($@, ':') + 2);
194             } else {
195 0         0 print STDERR $@;
196             }
197 0         0 return $class->osprey_usage(1, $usage);
198             }
199              
200 15 100       180 return $self unless $subcommand_class;
201              
202 5 100       35 use_module($subcommand_class) unless ref $subcommand_class;
203              
204 5         259 return $subcommand_class->new_with_options(
205             %params,
206             parent_command => $self,
207             invoked_as => "$params{invoked_as} $subcommand_name"
208             );
209             }
210              
211             sub parse_options {
212 16     16 0 1460 my ($class, %params) = @_;
213              
214 16         378 my %options = $class->_osprey_options;
215 16         532 my %config = $class->_osprey_config;
216 16         465 my %subcommands = $class->_osprey_subcommands;
217              
218 16         236 my ($options, $abbreviations) = _osprey_prepare_options(\%options, \%config);
219 16         57 @ARGV = _osprey_fix_argv(\%options, $abbreviations);
220              
221 16 100       49 my @getopt_options = %subcommands ? qw(require_order) : ();
222              
223 16 50       55 push @getopt_options, @{$config{getopt_options}} if defined $config{getopt_options};
  0         0  
224              
225 16         34 my $prog_name = $params{invoked_as};
226 16 100       41 $prog_name = Getopt::Long::Descriptive::prog_name() if !defined $prog_name;
227              
228 16         30 my $usage_str = $config{usage_string};
229 16 50       33 unless (defined $usage_str) {
230 16 100       33 if (%subcommands) {
231 11         33 $usage_str = "Usage: $prog_name %o [subcommand]";
232             } else {
233 5         19 $usage_str = "Usage: $prog_name %o";
234             }
235             }
236              
237 16         127 my ($opt, $usage) = describe_options(
238             $usage_str,
239             @$options,
240             [],
241             [ 'h', "show a short help message" ],
242             [ 'help', "show a long help message" ],
243             [ 'man', "show the manual" ],
244             { getopt_conf => \@getopt_options },
245             );
246              
247 16         13080 $usage->{prog_name} = $prog_name;
248 16         42 $usage->{target} = $class;
249              
250 16 50       40 if ($usage->{should_die}) {
251 0         0 return $class->osprey_usage(1, $usage);
252             }
253              
254 16         33 my %parsed_params;
255              
256 16         51 for my $name (keys %options, qw(h help man)) {
257 55         153 my $val = $opt->$name();
258 55 100       302 $parsed_params{$name} = $val if defined $val;
259             }
260              
261 16         103 return \%parsed_params, $usage;
262              
263             }
264              
265             sub osprey_usage {
266 0     0 0   my ($class, $code, @messages) = @_;
267              
268 0           my $usage;
269              
270 0 0 0       if (@messages && blessed($messages[0]) && $messages[0]->isa('CLI::Osprey::Descriptive::Usage')) {
      0        
271 0           $usage = shift @messages;
272             } else {
273 0           local @ARGV = ();
274 0           (undef, $usage) = $class->parse_options(help => 1);
275             }
276              
277 0           my $message;
278 0 0         $message = join("\n", @messages, '') if @messages;
279 0           $message .= $usage . "\n";
280              
281 0 0         if ($code) {
282 0           CORE::warn $message;
283             } else {
284 0           print $message;
285             }
286 0 0         exit $code if defined $code;
287 0           return;
288             }
289              
290             sub osprey_help {
291 0     0 0   my ($class, $code, $usage) = @_;
292              
293 0 0 0       unless (defined $usage && blessed($usage) && $usage->isa('CLI::Osprey::Descriptive::Usage')) {
      0        
294 0           local @ARGV = ();
295 0           (undef, $usage) = $class->parse_options(help => 1);
296             }
297              
298 0           my $message = $usage->option_help . "\n";
299              
300 0 0         if ($code) {
301 0           CORE::warn $message;
302             } else {
303 0           print $message;
304             }
305 0 0         exit $code if defined $code;
306 0           return;
307             }
308              
309             sub osprey_man {
310 0     0 0   my ($class, $usage, $output) = @_;
311              
312 0 0 0       unless (defined $usage && blessed($usage) && $usage->isa('CLI::Osprey::Descriptive::Usage')) {
      0        
313 0           local @ARGV = ();
314 0           (undef, $usage) = $class->parse_options(man => 1);
315             }
316              
317 0           my $tmpdir = Path::Tiny->tempdir;
318 0           my $podfile = $tmpdir->child("help.pod");
319 0           $podfile->spew_utf8($usage->option_pod);
320              
321 0           require Pod::Usage;
322 0           Pod::Usage::pod2usage(
323             -verbose => 2,
324             -input => "$podfile",
325             -exitval => 'NOEXIT',
326             -output => $output,
327             );
328              
329 0           exit(0);
330             }
331              
332             sub _osprey_subcommand_desc {
333 0     0     my ($class) = @_;
334 0           my %config = $class->_osprey_config;
335 0           return $config{desc};
336             }
337              
338             1;
339              
340             __END__