File Coverage

blib/lib/Getopt/Long/Subcommand.pm
Criterion Covered Total %
statement 84 143 58.7
branch 32 68 47.0
condition 15 28 53.5
subroutine 9 12 75.0
pod 1 1 100.0
total 141 252 55.9


line stmt bran cond sub pod time code
1             package Getopt::Long::Subcommand;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2021-05-30'; # DATE
5             our $DIST = 'Getopt-Long-Subcommand'; # DIST
6             our $VERSION = '0.104'; # VERSION
7              
8 1     1   82496 use 5.010001;
  1         13  
9 1     1   7 use strict;
  1         2  
  1         33  
10 1     1   6 use warnings;
  1         2  
  1         1720  
11             #use Log::ger;
12              
13             require Exporter;
14             our @ISA = qw(Exporter);
15             ## no critic (Modules::ProhibitAutomaticExportation)
16             our @EXPORT = qw(
17             GetOptions
18             );
19             ## use critic
20              
21             # XXX completion & configure are actually only allowed at the top-level
22             my @known_cmdspec_keys = qw(
23             options
24             subcommands
25             default_subcommand
26             summary description
27             completion
28             configure
29             );
30              
31             sub _cmdspec_opts_to_gl_ospec {
32 40     40   90 my ($cmdspec_opts, $is_completion, $res) = @_;
33             return { map {
34 40 50       111 if ($is_completion) {
  62         110  
35             # we don't want side-effects during completion (handler printing or
36             # existing, etc), so we set an empty coderef for all handlers.
37 0     0   0 ($_ => sub{});
38             } else {
39 62         87 my $k = $_;
40 62         86 my $v = $cmdspec_opts->{$k};
41 62 50       136 my $handler = ref($v) eq 'HASH' ? $v->{handler} : $v;
42 62 50       120 if (ref($handler) eq 'CODE') {
43 62         81 my $orig_handler = $handler;
44             $handler = sub {
45 7     7   2013 my ($cb, $val) = @_;
46 7         24 $orig_handler->($cb, $val, $res);
47 62         244 };
48             }
49 62         223 ($k => $handler);
50             }
51             } keys %$cmdspec_opts };
52             }
53              
54             sub _gl_getoptions {
55 40     40   1137 require Getopt::Long;
56              
57 40         11177 my ($ospec, $configure, $pass_through, $res) = @_;
58              
59             my @configure = @{
60 40   100     60 $configure //
  40         194  
61             ['no_ignore_case', 'no_getopt_compat', 'gnu_compat', 'bundling']
62             };
63 40 100       105 if ($pass_through) {
64             push @configure, 'pass_through'
65 34 50       54 unless grep { $_ eq 'pass_through' } @configure;
  136         283  
66             } else {
67 6         10 @configure = grep { $_ ne 'pass_through' } @configure;
  21         45  
68             }
69             #log_trace('[comp][glsubc] Performing Getopt::Long::GetOptions (configure: %s)',
70             # $pass_through, \@configure);
71              
72 40         115 my $old_conf = Getopt::Long::Configure(@configure);
73 40 100   0   3009 local $SIG{__WARN__} = sub {} if $pass_through;
74              
75             # ugh, this is ugly. the problem we're trying to solve: in the case of 'subc
76             # --help', 'subc' is consumed first by Getopt::Long and thus removed from
77             # @ARGV. when --help handler wants to find out the subcommand name ('subc'),
78             # it doesn't have anywhere to look for. so we give it in $res which is
79             # passed as the third argument to the handler.
80 40         113 local $res->{_non_options_argv} = [];
81              
82             #log_trace('[comp][glsubc] @ARGV before Getopt::Long::GetOptions: %s', \@ARGV);
83             #log_trace('[comp][glsubc] spec for Getopt::Long::GetOptions: %s', $ospec);
84             my $gl_res = Getopt::Long::GetOptions(
85             %$ospec,
86 59     59   6678 '<>' => sub { push @{ $res->{_non_options_argv} }, $_[0] },
  59         166  
87 40         226 );
88 40         1582 @ARGV = @{ $res->{_non_options_argv} };
  40         109  
89              
90             #log_trace('[comp][glsubc] @ARGV after Getopt::Long::GetOptions: %s', \@ARGV);
91 40         107 Getopt::Long::Configure($old_conf);
92 40         895 $gl_res;
93             }
94              
95             sub _GetOptions {
96 40     40   89 my ($cmdspec, $is_completion, $res, $stash) = @_;
97              
98 40   100     138 $res //= {success=>undef};
99 40   100     140 $stash //= {
100             path => '', # for displaying error message
101             level => 0,
102             };
103              
104             # check command spec
105             {
106             #log_trace("[comp][glsubc] Checking cmdspec keys: %s", [keys %$cmdspec]);
107 40         61 for my $k (keys %$cmdspec) {
  40         108  
108 679         1203 (grep { $_ eq $k } @known_cmdspec_keys)
109             or die "Unknown command specification key '$k'" .
110 97 0       161 ($stash->{path} ? " (under $stash->{path})" : "") . "\n";
    50          
111             }
112             }
113              
114             my $has_subcommands = $cmdspec->{subcommands} &&
115 40   66     114 keys(%{$cmdspec->{subcommands}});
116             #log_trace("TMP:has_subcommands=%s", $has_subcommands);
117 40   66     105 my $pass_through = $has_subcommands || $is_completion;
118              
119             my $ospec = _cmdspec_opts_to_gl_ospec(
120 40         100 $cmdspec->{options}, $is_completion, $res);
121 40 100       133 unless (_gl_getoptions(
122             $ospec, $cmdspec->{configure}, $pass_through, $res)) {
123 1         4 $res->{success} = 0;
124 1         8 return $res;
125             }
126              
127             # for doing completion
128 39 50       114 if ($is_completion) {
129 0   0     0 $res->{comp_ospec} //= {};
130 0         0 for (keys %$ospec) {
131 0         0 $res->{comp_ospec}{$_} = $ospec->{$_};
132             }
133             }
134              
135 39 100       81 if ($has_subcommands) {
136             # for doing completion of subcommand names
137 34 50       68 if ($is_completion) {
138             my $scnames = $res->{comp_subcommand_names}[$stash->{level}] =
139 0         0 [sort keys %{$cmdspec->{subcommands}}];
  0         0  
140             $res->{comp_subcommand_summaries}[$stash->{level}] =
141 0         0 [map {$cmdspec->{subcommands}{$_}{summary}} @$scnames];
  0         0  
142             }
143              
144 34   100     127 $res->{subcommand} //= [];
145              
146 34         54 my $push;
147             my $sc_name;
148              
149 34 50       109 if (defined $res->{subcommand}[ $stash->{level} ]) {
    100          
    50          
150             # subcommand has been set, e.g. by option handler
151 0         0 $sc_name = $res->{subcommand}[ $stash->{level} ];
152             } elsif (@ARGV) {
153 29         51 $sc_name = shift @ARGV;
154 29         52 $push++; # we need to push to $res->{subcommand} later
155             } elsif (defined $cmdspec->{default_subcommand}) {
156 0         0 $sc_name = $cmdspec->{default_subcommand};
157 0         0 $push++;
158             } else {
159             # no subcommand
160 5         9 $res->{success} = 1;
161 5         26 return $res;
162             }
163              
164             # for doing completion of subcommand names
165 29 50       58 if ($is_completion) {
166 0         0 push @{ $res->{comp_subcommand_name} }, $sc_name;
  0         0  
167             }
168              
169 29         62 my $sc_spec = $cmdspec->{subcommands}{$sc_name};
170 29 100       62 unless ($sc_spec) {
171             warn "Unknown subcommand '$sc_name'".
172 7 100       297 ($stash->{path} ? " for $stash->{path}":"")."\n"
    50          
173             unless $is_completion;
174 7         36 $res->{success} = 0;
175 7         52 return $res;
176             };
177 22 50       42 push @{ $res->{subcommand} }, $sc_name if $push;
  22         51  
178 22 100       70 local $stash->{path} = ($stash->{path} ? "/" : "") . $sc_name;
179 22         45 local $stash->{level} = $stash->{level}+1;
180 22         78 _GetOptions($sc_spec, $is_completion, $res, $stash);
181             }
182 27   100     73 $res->{success} //= 1;
183              
184             #log_trace('[comp][glsubc] Final @ARGV: %s', \@ARGV) unless $stash->{path};
185             #log_trace('[comp][glsubc] TMP: stash=%s', $stash);
186             #log_trace('[comp][glsubc] TMP: res=%s', $res);
187 27         117 $res;
188             }
189              
190             sub GetOptions {
191 18     18 1 66375 my %cmdspec = @_;
192              
193             # figure out if we run in completion mode
194 18         41 my ($is_completion, $shell, $words, $cword);
195             CHECK_COMPLETION:
196             {
197 18 50       27 if ($ENV{COMP_SHELL}) {
  18 50       59  
198 0         0 ($shell = $ENV{COMP_SHELL}) =~ s!.+/!!;
199             } elsif ($ENV{COMMAND_LINE}) {
200 0         0 $shell = 'tcsh';
201             } else {
202 18         33 $shell = 'bash';
203             }
204              
205 18 50 33     89 if ($ENV{COMP_LINE} || $ENV{COMMAND_LINE}) {
206 0 0       0 if ($ENV{COMP_LINE}) {
    0          
207 0         0 $is_completion++;
208 0         0 require Complete::Bash;
209 0         0 ($words, $cword) = @{ Complete::Bash::parse_cmdline(
  0         0  
210             undef, undef, {truncate_current_word=>1}) };
211 0         0 ($words, $cword) = @{ Complete::Bash::join_wordbreak_words(
  0         0  
212             $words, $cword) };
213             } elsif ($ENV{COMMAND_LINE}) {
214 0         0 $is_completion++;
215 0         0 require Complete::Tcsh;
216 0         0 $shell = 'tcsh';
217 0         0 ($words, $cword) = @{ Complete::Tcsh::parse_cmdline() };
  0         0  
218             } else {
219 0         0 last CHECK_COMPLETION;
220             }
221              
222 0         0 shift @$words; $cword--; # strip program name
  0         0  
223 0         0 @ARGV = @$words;
224             }
225             }
226              
227 18         39 my $res = _GetOptions(\%cmdspec, $is_completion);
228              
229 18 50       42 if ($is_completion) {
230 0         0 my $ospec = $res->{comp_ospec};
231 0         0 require Complete::Getopt::Long;
232             my $compres = Complete::Getopt::Long::complete_cli_arg(
233             words => $words, cword => $cword, getopt_spec=>$ospec,
234             extras => {
235             stash => $res->{stash},
236             },
237             bundling => do {
238 0 0       0 if (!$cmdspec{configure}) {
    0          
    0          
239 0         0 1;
240 0         0 } elsif (grep { $_ eq 'bundling' } @{ $cmdspec{configure} }) {
  0         0  
241 0         0 1;
242 0         0 } elsif (grep { $_ eq 'no_bundling' } @{ $cmdspec{configure} }) {
  0         0  
243 0         0 0;
244             } else {
245 0         0 0;
246             }
247             },
248             completion => sub {
249 0     0   0 my %args = @_;
250              
251 0   0     0 my $word = $args{word} // '';
252 0         0 my $type = $args{type};
253 0         0 my $stash = $args{stash};
254              
255             # complete subcommand names
256 0 0 0     0 if ($type eq 'arg' &&
257 0   0     0 $args{argpos} < @{$res->{comp_subcommand_names}//[]}) {
258 0         0 require Complete::Util;
259             return Complete::Util::complete_array_elem(
260             word => $res->{comp_subcommand_name}[$args{argpos}],
261             array => $res->{comp_subcommand_names}[$args{argpos}],
262 0         0 summaries => $res->{comp_subcommand_summaries}[$args{argpos}]
263             );
264             }
265              
266 0         0 $args{getopt_res} = $res;
267 0         0 $args{subcommand} = $res->{comp_subcommand_name};
268 0 0       0 $cmdspec{completion}->(%args) if $cmdspec{completion};
269             },
270 0         0 );
271              
272 0 0       0 if ($shell eq 'bash') {
    0          
273 0         0 print Complete::Bash::format_completion($compres);
274             } elsif ($shell eq 'tcsh') {
275 0         0 print Complete::Tcsh::format_completion($compres);
276             } else {
277 0         0 die "Unknown shell '$shell'";
278             }
279              
280 0         0 exit 0;
281             }
282              
283             # cleanup unneeded details
284 18         63 $res;
285             }
286              
287             1;
288             # ABSTRACT: Process command-line options, with subcommands and completion
289              
290             __END__