File Coverage

blib/lib/Getopt/Guided.pm
Criterion Covered Total %
statement 114 114 100.0
branch 64 66 96.9
condition 16 16 100.0
subroutine 14 14 100.0
pod 0 7 0.0
total 208 217 95.8


line stmt bran cond sub pod time code
1             # Prefer numeric version for backwards compatibility
2 13     13   1344161 BEGIN { require 5.010000 }; ## no critic ( RequireUseStrict, RequireUseWarnings )
3 13     13   72 use strict;
  13         35  
  13         496  
4 13     13   62 use warnings;
  13         28  
  13         6397  
5              
6             package Getopt::Guided;
7              
8             $Getopt::Guided::VERSION = 'v3.1.2';
9              
10             # Options Delimiter constant
11             sub OD () { '-' }
12             # End Of Options Delimiter constant
13             sub EOOD () { '--' }
14             # Flag Indicator Character Class constant
15             sub FICC () { '[!+]' }
16             # Option-Argument Indicator Character Class constant
17             sub OAICC () { '[,:]' }
18             # Perl boolean true constant ( IV == 1 )
19             sub TRUE () { !!1 }
20             # Perl boolean false constant
21             sub FALSE () { !!0 }
22             # Common exit status constants
23             sub EXIT_SUCCESS () { 0 }
24             sub EXIT_FAILURE () { 1 }
25             sub EXIT_USAGE () { 2 }
26              
27             @Getopt::Guided::EXPORT_OK =
28             qw( EOOD EXIT_SUCCESS EXIT_FAILURE EXIT_USAGE getopts processopts readopts print_version_info );
29              
30             sub program_name () {
31 14     14 0 117 require File::Basename;
32 14         1214 File::Basename::basename( $0 )
33             }
34              
35             sub croakf ( $@ ) {
36 13 100   13 0 147371 @_ = ( ( @_ == 1 ? shift : sprintf shift, @_ ) . ', stopped' );
37 13         108 require Carp;
38 13         1917 goto &Carp::croak
39             }
40              
41             sub import {
42 13     13   1776 my $module = shift;
43              
44 13         24 our @EXPORT_OK;
45 13         41 my $target = caller;
46 13         127 for my $function ( @_ ) {
47             croakf "%s: '%s' is not exported", $module, $function
48 38 100       91 unless grep { $function eq $_ } @EXPORT_OK;
  304         607  
49 13     13   104 no strict 'refs'; ## no critic ( ProhibitNoStrict )
  13         26  
  13         2219  
50 37         205 *{ "$target\::$function" } = $module->can( $function )
  37         77964  
51             }
52             }
53              
54             # Implementation is based on m//gc with \G
55             sub parse_spec ( $;\%$ ) {
56 64     64 0 167 my ( $spec, $spec_as_hash, $spec_length_expected ) = @_;
57              
58 64 100       186 $spec_as_hash = {} unless defined $spec_as_hash;
59              
60 64         106 my $spec_length_got;
61 13     13   119 no warnings qw( uninitialized ); ## no critic ( ProhibitNoWarnings )
  13         29  
  13         22359  
62 64         354 while ( $spec =~ m/\G ( [[:alnum:]] ) ( ${ \( FICC ) } | ${ \( OAICC ) } | )/gcox ) {
  8         42  
  8         2430  
63 115         355 my ( $name, $indicator ) = ( $1, $2 );
64             croakf "%s parameter contains option '%s' multiple times", '$spec', $name
65 115 100       274 if exists $spec_as_hash->{ $name };
66 114         276 $spec_as_hash->{ $name } = $indicator;
67 114         339 ++$spec_length_got
68             }
69 63         130 my $offset = pos $spec;
70 63 100 100     419 croakf "%s parameter isn't a non-empty string of alphanumeric characters", '$spec'
71             unless defined $offset and $offset == length $spec;
72 57 100 100     195 croakf '%s parameter specifies %d options (expected: %d)', '$spec', $spec_length_got, $spec_length_expected
73             if defined $spec_length_expected and $spec_length_got != $spec_length_expected;
74              
75 56         180 $spec_as_hash
76             }
77              
78             # https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02>
79             sub getopts ( $\%;\@ ) {
80 50     50 0 1019276 my ( $spec, $opts, $argv ) = @_;
81              
82 50 100       225 my $spec_as_hash = ref $spec eq 'HASH' ? $spec : parse_spec $spec;
83 44 100       130 croakf "%s parameter isn't an empty hash", '%$opts'
84             if %$opts;
85 43 100       126 $argv = \@ARGV unless defined $argv;
86              
87 43         124 my @argv_backup = @$argv;
88 43         82 my @error;
89             # Guideline 4, Guideline 9
90 43   100     349 while ( @$argv and my ( $name, $rest ) = ( $argv->[ 0 ] =~ m/\A ${ \( OD ) } (.) (.*)/ox ) ) {
  6         168  
91             # Guideline 10
92 123 100       315 shift @$argv, last
93             if $argv->[ 0 ] eq EOOD;
94             @error = ( 'illegal option', $name ), last
95 122 100       251 unless exists $spec_as_hash->{ $name }; ## no critic ( ProhibitNegativeExpressionsInUnlessAndUntilConditions )
96              
97 118         185 my $indicator = $spec_as_hash->{ $name };
98 118 100       282 if ( $indicator =~ m/\A ${ \( OAICC ) } \z/ox ) {
  5         140  
99             # Case: Option has an option-argument
100             # Shift delimeted option name
101 55         86 shift @$argv;
102             # Extract option-argument value
103 55         82 my $value;
104 55 100       106 if ( $rest ne '' ) {
105 6         13 $value = $rest;
106             } else {
107             # Guideline 7
108 49 100       121 @error = ( 'option requires an argument', $name ), last
109             unless @$argv;
110             # Guideline 6, Guideline 8
111 47 100       108 @error = ( 'option requires an argument', $name ), last
112             unless defined( $value = shift @$argv );
113             }
114             # Store option-argument value
115 52 100       105 if ( $indicator eq ':' ) {
116             # Standard behaviour: Overwrite option-argument
117 31         163 $opts->{ $name } = $value
118             } else {
119             # Create and fill list of option-arguments ( $indicator eq ',' )
120 21 100       53 $opts->{ $name } = [] unless exists $opts->{ $name };
121 21         30 push @{ $opts->{ $name } }, $value
  21         105  
122             }
123             } else {
124             # Case: Option is a flag
125 63 100       200 if ( not exists $opts->{ $name } ) {
    100          
    100          
126             # Initialisation
127 45         122 $opts->{ $name } = TRUE
128             } elsif ( $indicator eq '!' ) {
129             # Negate logically
130 1         2 $opts->{ $name } = not $opts->{ $name }
131             } elsif ( $indicator eq '+' ) {
132             # Increment
133 14         23 ++$opts->{ $name }
134             }
135             # Guideline 5
136 63 100       126 if ( $rest eq '' ) {
137             # Shift delimeted option name
138 47         210 shift @$argv
139             } else {
140 16         99 $argv->[ 0 ] = OD . $rest ## no critic ( RequireLocalizedPunctuationVars )
141             }
142             }
143              
144             }
145              
146 43 100       118 if ( @error ) {
147             # Restore to avoid side effects
148 7         22 @$argv = @argv_backup; ## no critic ( RequireLocalizedPunctuationVars )
149 7         20 %$opts = ();
150             # Prepare and print warning message:
151             # Program name, type of error, and invalid option character
152 7         25 warn sprintf( "%s: %s -- %s\n", program_name(), @error ) ## no critic ( RequireCarping )
153             }
154              
155 43         692 @error == 0
156             }
157              
158             # Prepare and print version info message:
159             # - Program name and version (first line)
160             # - Interpreter name ("perl") and version (second line)
161             sub print_version_info {
162             # call frame 0: processopts() (the caller) calls print_version_info()
163             # the caller's package name is Getopt::Guided
164             # call frame 1: a run() function/method (the caller) usually calls processopts()
165             # the caller's package name is whatever it is (could be "main")
166 3   100 3 0 239867 printf STDOUT "%s %s\nperl v%vd\n", program_name(), ( caller( 1 ) // 'main' )->VERSION, $^V;
167 3         139 EOOD
168             }
169              
170             sub processopts ( \@@ ) {
171 17     17 0 882096 my $argv = shift;
172 17         35 my $spec_as_array = do { my $t = 0; [ grep $t ^= 1, @_ ] }; ## no critic ( RequireBlockGrep )
  17         31  
  17         119  
173              
174             # Check each option specification individually (1)
175 17         30 my $spec_as_hash;
176 17         77 parse_spec $_, %$spec_as_hash, 1 for @$spec_as_array;
177              
178 15 100       58 return FALSE unless getopts $spec_as_hash, my %opts, @$argv;
179              
180             # This ordered processing is a feature!
181 13         46 for ( my $i = 0 ; $i < @_ ; $i += 2 ) {
182             # If $_[ $i ] refers to a flag with no indicator, the split still returns
183             # the empty string (not undef!) as the value for the indicator
184 18         5507 my ( $name, $indicator ) = split //, $_[ $i ];
185 18 100       55 if ( exists $opts{ $name } ) {
186 16         54 my $value = delete $opts{ $name };
187 16         39 my $dest = $_[ $i + 1 ];
188 16         34 my $dest_ref_type = ref $dest;
189 16 100 100     77 if ( $dest_ref_type eq 'SCALAR' ) {
    100          
    100          
190 4         10 ${ $dest } = $value
  4         16  
191             } elsif ( $dest_ref_type eq 'ARRAY' and $indicator eq ',' ) {
192 1         3 @{ $dest } = @$value
  1         6  
193             } elsif ( $dest_ref_type eq 'CODE' ) {
194             # Callbacks are called in scalar context
195             # If EOOD is the return value of the callback,
196             # processopts() will terminate early. The return value will be
197             # a special TRUE value. Using $name is not sufficient because 0 and 1
198             # a posible values for $name.
199 10 100 100     37 return ( OD . $name )
200             if ( $dest->( $value, $name, $indicator ) // '' ) eq EOOD
201             } else {
202 1         6 croakf "'%s' is an unsupported destination reference type for the '%s' indicator", $dest_ref_type, $indicator
203             }
204             }
205             }
206              
207 8         4238 TRUE
208             }
209              
210             sub readopts ( \@ ) {
211 4     4 0 255068 my ( $argv ) = @_;
212              
213 4         33 require File::Spec::Functions;
214 4 100       15 return unless -f ( my $file = File::Spec::Functions::catfile( $ENV{ XDG_CONFIG_HOME }, program_name() . 'rc' ) );
215              
216 3 50   1   169 open my $fh, '<:encoding(UTF-8)', $file ## no critic ( RequireBriefOpen )
  1         888  
  1         37  
  1         7  
217             or croakf "Cannot open file '%s' for reading (%s)", $file, $!;
218              
219 3         1770 while ( <$fh> ) {
220 5         40 chomp;
221 5 50       32 next if m/\A (?: \#.* | ) \z/x;
222 5 100       29 if ( m/\A ( [[:alnum:]] ) (?: \ ( .+ ) )? \z/x ) {
223 4 100       24 unshift @$argv, ( OD . $1, defined $2 ? $2 : () );
224             next
225 4         34 }
226 1         8 croakf "File '%s' contains the invalid line '%s'", $file, $_
227             }
228              
229             return
230 2         47 }
231              
232             1