File Coverage

blib/lib/Getopt/Long/Util.pm
Criterion Covered Total %
statement 90 142 63.3
branch 61 96 63.5
condition 27 46 58.7
subroutine 7 10 70.0
pod 5 5 100.0
total 190 299 63.5


line stmt bran cond sub pod time code
1             package Getopt::Long::Util;
2              
3 1     1   59690 use 5.010001;
  1         11  
4 1     1   4 use strict;
  1         2  
  1         16  
5 1     1   4 use warnings;
  1         1  
  1         347  
6              
7             require Exporter;
8             our @ISA = qw(Exporter);
9             our @EXPORT_OK = qw(
10             parse_getopt_long_opt_spec
11             humanize_getopt_long_opt_spec
12             detect_getopt_long_script
13             gen_getopt_long_spec_from_getopt_std_spec
14             array_getopt_long_spec_to_hash
15             );
16              
17             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
18             our $DATE = '2022-10-09'; # DATE
19             our $DIST = 'Getopt-Long-Util'; # DIST
20             our $VERSION = '0.897'; # VERSION
21              
22             our %SPEC;
23              
24             $SPEC{parse_getopt_long_opt_spec} = {
25             v => 1.1,
26             summary => 'Parse a single Getopt::Long option specification',
27             description => <<'_',
28              
29             Will produce a hash with some keys:
30              
31             * `is_arg` (if true, then option specification is the special `<>` for argument
32             callback)
33             * `opts` (array of option names, in the order specified in the opt spec)
34             * `type` (string, type name)
35             * `desttype` (either '', or '@' or '%'),
36             * `is_neg` (true for `--opt!`)
37             * `is_inc` (true for `--opt+`)
38             * `min_vals` (int, usually 0 or 1)
39             * `max_vals` (int, usually 0 or 1 except for option that requires multiple
40             values)
41              
42             Will return undef if it can't parse the string.
43              
44             _
45             args => {
46             optspec => {
47             schema => 'str*',
48             req => 1,
49             pos => 0,
50             },
51             },
52             args_as => 'array',
53             result_naked => 1,
54             result => {
55             schema => 'hash*',
56             },
57             examples => [
58             {
59             args => {optspec => 'help|h|?'},
60             result => {dash_prefix=>'', opts=>['help', 'h', '?']},
61             },
62             {
63             args => {optspec=>'--foo=s'},
64             result => {dash_prefix=>'--', opts=>['foo'], type=>'s', desttype=>''},
65             },
66             ],
67             };
68             # BEGIN_BLOCK: parse_getopt_long_opt_spec
69             sub parse_getopt_long_opt_spec {
70 25     25 1 975 my $optspec = shift;
71 25 100       57 return {is_arg=>1, dash_prefix=>'', opts=>[]}
72             if $optspec eq '<>';
73 23 100       248 $optspec =~ qr/\A
74             (?P-{0,2})
75             (?P[A-Za-z0-9_][A-Za-z0-9_-]*)
76             (?P (?: \| (?:[^:|!+=:-][^:|!+=:]*) )*)?
77             (?:
78             (?P!) |
79             (?P\+) |
80             (?:
81             =
82             (?P[siof])
83             (?P|[%@])?
84             (?:
85             \{
86             (?: (?P\d+), )?
87             (?P\d+)
88             \}
89             )?
90             ) |
91             (?:
92             :
93             (?P[siof])
94             (?P|[%@])?
95             ) |
96             (?:
97             :
98             (?P-?\d+)
99             (?P|[%@])?
100             ) |
101             (?:
102             :
103             (?P\+)
104             (?P|[%@])?
105             )
106             )?
107             \z/x
108             or return;
109 1     1   383 my %res = %+;
  1         350  
  1         1421  
  21         317  
110              
111 21 100       81 if (defined $res{optnum}) {
112 1         3 $res{type} = 'i';
113             }
114              
115 21 100       37 if ($res{aliases}) {
116 7         9 my @als;
117 7         18 for my $al (split /\|/, $res{aliases}) {
118 18 100       32 next unless length $al;
119 11 50       17 next if $al eq $res{name};
120 11 50       19 next if grep {$_ eq $al} @als;
  4         10  
121 11         22 push @als, $al;
122             }
123 7         21 $res{opts} = [$res{name}, @als];
124             } else {
125 14         29 $res{opts} = [$res{name}];
126             }
127 21         35 delete $res{name};
128 21         22 delete $res{aliases};
129              
130 21 100       51 $res{is_neg} = 1 if $res{is_neg};
131 21 100       31 $res{is_inc} = 1 if $res{is_inc};
132              
133 21         78 \%res;
134             }
135             # END_BLOCK: parse_getopt_long_opt_spec
136              
137             $SPEC{humanize_getopt_long_opt_spec} = {
138             v => 1.1,
139             description => <<'_',
140              
141             Convert option specification into a more human-friendly
142             notation that is suitable for including in help/usage text, for example:
143              
144             help|h|? -> "--help, -h, -?"
145             help|h|? -> "--help | -h | -?" # if you provide 'separator'
146             --foo=s -> "--foo=s"
147             --foo=s -> "--foo=somelabel" # if you provide 'value_label'
148             --foo:s -> "--foo[=s]"
149             --foo=s@ -> "(--foo=s)+"
150             --foo=s% -> "(--foo key=value)+"
151             --foo=s% -> "(--foo somelabel1=somelabel2)+" # if you provide 'key_label' and 'value_label'
152             --debug! -> "--(no)debug"
153              
154             It also produces POD-formatted string for use in POD documentation:
155              
156             --foo=s -> {plaintext=>"--foo=s", pod=>"B<--foo>=I"}
157             # if you set 'extended' to true
158              
159             Will die if can't parse the optspec string.
160              
161             _
162             args => {
163             optspec => {
164             schema => 'str*',
165             req => 1,
166             pos => 0,
167             },
168             separator => {
169             schema => 'str*',
170             default => ', ',
171             },
172             key_label => {
173             schema => 'str*',
174             default => 'key',
175             },
176             value_label => {
177             schema => 'str*',
178             },
179             value_label_link => {
180             schema => 'str*', # XXX url? podlink?
181             },
182             extended => {
183             summary => 'If set to true, will return a hash of multiple formats instead of a single plaintext format',
184             schema => 'bool*',
185             },
186             },
187             args_as => 'array',
188             result_naked => 1,
189             result => {
190             schema => ['any*', {of=>[['str*'], ['hash*', {of=>'str*'}]]}],
191             },
192             };
193             sub humanize_getopt_long_opt_spec {
194 13 100   13 1 2283 my $opts = {}; $opts = shift if ref $_[0] eq 'HASH';
  13         34  
195 13         20 my $optspec = shift;
196              
197 13 50       21 my $parse = parse_getopt_long_opt_spec($optspec)
198             or die "Can't parse opt spec $optspec";
199              
200 13 100       26 return "argument" if $parse->{is_arg};
201              
202 12         17 my $plain_res = '';
203 12         13 my $pod_res = '';
204 12         15 my $i = 0;
205 12         14 for (@{ $parse->{opts} }) {
  12         22  
206 18         19 $i++;
207 18         22 my $opt_plain_res = '';
208 18         18 my $opt_pod_res = '';
209 18 100 100     41 if ($parse->{is_neg} && length($_) > 1) {
210 2         4 $opt_plain_res .= "--(no)$_";
211 2         3 $opt_pod_res .= "B<--(no)$_>";
212             } else {
213 16 100       38 if (length($_) > 1) {
214 11         17 $opt_plain_res .= "--$_";
215 11         15 $opt_pod_res .= "B<--$_>";
216             } else {
217 5         8 $opt_plain_res .= "-$_";
218 5         6 $opt_pod_res .= "B<-$_>";
219             }
220 16 100 66     44 if ($i==1 && ($parse->{type} || $parse->{opttype})) {
      66        
221             # show value label
222 8   100     24 my $key_label = $opts->{key_label} // 'key';
223             my $value_label = $opts->{value_label} //
224 8   66     23 $parse->{type} // $parse->{opttype};
      33        
225              
226 8 50       13 $opt_plain_res .= "[" if $parse->{opttype};
227 8 100 66     23 $opt_plain_res .= ($parse->{type} && $parse->{desttype} eq '%' ? " " : "=");
228 8 100       17 $opt_plain_res .= "$key_label=" if $parse->{desttype} eq '%';
229 8         9 $opt_plain_res .= $value_label;
230 8 50       14 $opt_plain_res .= "]" if $parse->{opttype};
231              
232 8 50       10 $opt_pod_res .= "[" if $parse->{opttype};
233 8 100 66     24 $opt_pod_res .= ($parse->{type} && $parse->{desttype} eq '%' ? " " : "=");
234 8 100       13 $opt_pod_res .= "I<$key_label>=" if $parse->{desttype} eq '%';
235 8 50       17 $opt_pod_res .= defined $opts->{value_label_link} ? "L<$value_label|$opts->{value_label_link}>" : "I<$value_label>";
236 8 50       15 $opt_pod_res .= "]" if $parse->{opttype};
237             }
238 16 100 100     51 $opt_plain_res = "($opt_plain_res)+" if ($parse->{desttype} // '') =~ /@|%/;
239 16 100 100     45 $opt_pod_res = "($opt_pod_res)+" if ($parse->{desttype} // '') =~ /@|%/;
240             }
241              
242 18 100 50     40 $plain_res .= ($opts->{separator} // ", ") if length($plain_res);
243 18 100 50     34 $pod_res .= ($opts->{separator} // ", ") if length($pod_res);
244              
245 18         21 $plain_res .= $opt_plain_res;
246 18         26 $pod_res .= $opt_pod_res;
247             }
248              
249 12 100       23 if ($opts->{extended}) {
250             return {
251 1         9 plaintext => $plain_res,
252             pod => $pod_res,
253             };
254             } else {
255 11         51 $plain_res;
256             }
257             }
258              
259             $SPEC{detect_getopt_long_script} = {
260             v => 1.1,
261             summary => 'Detect whether a file is a Getopt::Long-based CLI script',
262             description => <<'_',
263              
264             The criteria are:
265              
266             * the file must exist and readable;
267              
268             * (optional, if `include_noexec` is false) file must have its executable mode
269             bit set;
270              
271             * content must start with a shebang C<#!>;
272              
273             * either: must be perl script (shebang line contains 'perl') and must contain
274             something like `use Getopt::Long`;
275              
276             _
277             args => {
278             filename => {
279             summary => 'Path to file to be checked',
280             schema => 'str*',
281             pos => 0,
282             cmdline_aliases => {f=>{}},
283             },
284             string => {
285             summary => 'String to be checked',
286             schema => 'buf*',
287             },
288             include_noexec => {
289             summary => 'Include scripts that do not have +x mode bit set',
290             schema => 'bool*',
291             default => 1,
292             },
293             },
294             args_rels => {
295             'req_one' => ['filename', 'string'],
296             },
297             };
298             sub detect_getopt_long_script {
299 0     0 1 0 my %args = @_;
300              
301 0 0 0     0 (defined($args{filename}) xor defined($args{string}))
302             or return [400, "Please specify either filename or string"];
303 0   0     0 my $include_noexec = $args{include_noexec} // 1;
304              
305 0         0 my $yesno = 0;
306 0         0 my $reason = "";
307 0         0 my %extrameta;
308              
309 0         0 my $str = $args{string};
310             DETECT:
311             {
312 0 0       0 if (defined $args{filename}) {
  0         0  
313 0         0 my $fn = $args{filename};
314 0 0       0 unless (-f $fn) {
315 0         0 $reason = "'$fn' is not a file";
316 0         0 last;
317             };
318 0 0 0     0 if (!$include_noexec && !(-x _)) {
319 0         0 $reason = "'$fn' is not an executable";
320 0         0 last;
321             }
322 0         0 my $fh;
323 0 0       0 unless (open $fh, "<", $fn) {
324 0         0 $reason = "Can't be read";
325 0         0 last;
326             }
327             # for efficiency, we read a bit only here
328 0         0 read $fh, $str, 2;
329 0 0       0 unless ($str eq '#!') {
330 0         0 $reason = "Does not start with a shebang (#!) sequence";
331 0         0 last;
332             }
333 0         0 my $shebang = <$fh>;
334 0 0       0 unless ($shebang =~ /perl/) {
335 0         0 $reason = "Does not have 'perl' in the shebang line";
336 0         0 last;
337             }
338 0         0 seek $fh, 0, 0;
339             {
340 0         0 local $/;
  0         0  
341 0         0 $str = <$fh>;
342             }
343 0         0 close $fh;
344             }
345 0 0       0 unless ($str =~ /\A#!/) {
346 0         0 $reason = "Does not start with a shebang (#!) sequence";
347 0         0 last;
348             }
349 0 0       0 unless ($str =~ /\A#!.*perl/) {
350 0         0 $reason = "Does not have 'perl' in the shebang line";
351 0         0 last;
352             }
353              
354             # NOTE: the presence of \s* pattern after ^ causes massive slowdown of
355             # the regex when we reach many thousands of lines, so we use split()
356              
357             #if ($str =~ /^\s*(use|require)\s+(Getopt::Long(?:::Complete)?)(\s|;)/m) {
358             # $yesno = 1;
359             # $extrameta{'func.module'} = $2;
360             # last DETECT;
361             #}
362              
363 0         0 for (split /^/, $str) {
364 0 0       0 if (/^\s*(use|require)\s+(Getopt::Long(?:::Complete|::Less|::EvenLess)?)(\s|;|$)/) {
365 0         0 $yesno = 1;
366 0         0 $extrameta{'func.module'} = $2;
367 0         0 last DETECT;
368             }
369             }
370              
371 0         0 $reason = "Can't find any statement requiring Getopt::Long(?::Complete|::Less|::EvenLess)? module";
372             } # DETECT
373              
374 0         0 [200, "OK", $yesno, {"func.reason"=>$reason, %extrameta}];
375             }
376              
377             $SPEC{gen_getopt_long_spec_from_getopt_std_spec} = {
378             v => 1.1,
379             summary => 'Generate Getopt::Long spec from Getopt::Std spec',
380             args => {
381             spec => {
382             summary => 'Getopt::Std spec string',
383             schema => 'str*',
384             req => 1,
385             pos => 0,
386             },
387             is_getopt => {
388             summary => 'Whether to assume spec is for getopt() or getopts()',
389             description => <<'_',
390              
391             By default spec is assumed to be for getopts() instead of getopt(). This means
392             that for a spec like `abc:`, `a` and `b` don't take argument while `c` does. But
393             if `is_getopt` is true, the meaning of `:` is reversed: `a` and `b` take
394             arguments while `c` doesn't.
395              
396             _
397             schema => 'bool',
398             },
399             },
400             result_naked => 1,
401             result => {
402             schema => 'hash*',
403             },
404             };
405             sub gen_getopt_long_spec_from_getopt_std_spec {
406 0     0 1 0 my %args = @_;
407              
408 0         0 my $is_getopt = $args{is_getopt};
409 0         0 my $spec = {};
410              
411 0         0 while ($args{spec} =~ /(.)(:?)/g) {
412             $spec->{$1 . ($is_getopt ? ($2 ? "" : "=s") : ($2 ? "=s" : ""))} =
413 0 0   0   0 sub {};
    0          
    0          
414             }
415              
416 0         0 $spec;
417             }
418              
419             $SPEC{array_getopt_long_spec_to_hash} = {
420             v => 1.1,
421             summary => 'Convert array form of Getopt::Long spec to hash',
422             description => <<'_',
423              
424             's `GetOptions` function accepts a list of arguments. The first
425             optional argument is a hash for option storage. After that, a list of option
426             specs (e.g. `foo=s`), each optionally followed by a reference to specify
427             destination (e.g. a reference to scalar, or array, or code).
428              
429             Die on failure (e.g. invalid option spec).
430              
431             This routine converts that array into a hash of option specs as keys and
432             destinations as values. If an option spec does not have a destination, its
433             destination is set to `undef`. If hash storage is specified then the destination
434             will fall back to the hash storage's appropriate key when a specific destination
435             is not specified.
436              
437             Note that by converting to hash, 1) duplicate option specs are merged; and 2)
438             order of option specs is not preserved.
439              
440             _
441             args => {
442             spec => {
443             summary => 'Getopt::Long spec',
444             schema => 'array*',
445             req => 1,
446             pos => 0,
447             },
448             },
449             args_as => 'array',
450             result_naked => 1,
451             result => {
452             schema => 'hash*',
453             },
454             };
455             sub array_getopt_long_spec_to_hash {
456 4     4 1 9679 my $go_spec = [ @_ ];
457 4         8 my $hash_spec = {};
458              
459 4         5 my $hash_storage;
460 4 100 100     19 $hash_storage = shift @$go_spec
461             if @$go_spec && ref $go_spec->[0] eq 'HASH';
462              
463 4         9 while (@$go_spec) {
464 9         13 my $opt_spec = shift @$go_spec;
465 9         10 my $dest;
466 9 100 66     30 if (@$go_spec && ref $go_spec->[0]) {
    100          
467 5         8 $dest = shift @$go_spec;
468             } elsif ($hash_storage) {
469 2 50       4 my $res = parse_getopt_long_opt_spec($opt_spec)
470             or die "Invalid option spec '$opt_spec'";
471 2         4 my $name = $res->{opts}[0];
472 2 100       6 $hash_storage->{$name} = undef unless exists $hash_storage->{$name};
473             $dest = ref $hash_storage->{$name} ?
474             $hash_storage->{$name} :
475 2 100       9 \($hash_storage->{$name});
476             }
477 9         20 $hash_spec->{$opt_spec} = $dest;
478             }
479              
480 4         17 $hash_spec;
481             }
482              
483             1;
484             # ABSTRACT: Utilities for Getopt::Long
485              
486             __END__