File Coverage

blib/lib/Getopt/Guided.pm
Criterion Covered Total %
statement 113 114 99.1
branch 63 66 95.4
condition 16 16 100.0
subroutine 14 14 100.0
pod 0 7 0.0
total 206 217 94.9


line stmt bran cond sub pod time code
1             # Prefer numeric version for backwards compatibility
2 13     13   1281364 BEGIN { require 5.010000 }; ## no critic ( RequireUseStrict, RequireUseWarnings )
3 13     13   69 use strict;
  13         28  
  13         469  
4 13     13   64 use warnings;
  13         54  
  13         6234  
5              
6             package Getopt::Guided;
7              
8             $Getopt::Guided::VERSION = 'v3.1.0';
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 12     12 0 106 require File::Basename;
32 12         1038 File::Basename::basename( $0 )
33             }
34              
35             sub croakf ( $@ ) {
36 12 100   12 0 238716 @_ = ( ( @_ == 1 ? shift : sprintf shift, @_ ) . ', stopped' );
37 12         100 require Carp;
38 12         1409 goto &Carp::croak
39             }
40              
41             sub import {
42 13     13   2530 my $module = shift;
43              
44 13         22 our @EXPORT_OK;
45 13         42 my $target = caller;
46 13         122 for my $function ( @_ ) {
47             croakf "%s: '%s' is not exported", $module, $function
48 38 100       118 unless grep { $function eq $_ } @EXPORT_OK;
  304         619  
49 13     13   96 no strict 'refs'; ## no critic ( ProhibitNoStrict )
  13         26  
  13         2256  
50 37         227 *{ "$target\::$function" } = $module->can( $function )
  37         157568  
51             }
52             }
53              
54             # Implementation is based on m//gc with \G
55             sub parse_spec ( $;\%$ ) {
56 64     64 0 169 my ( $spec, $spec_as_hash, $spec_length_expected ) = @_;
57              
58 64 100       210 $spec_as_hash = {} unless defined $spec_as_hash;
59              
60 64         99 my $spec_length_got;
61 13     13   103 no warnings qw( uninitialized ); ## no critic ( ProhibitNoWarnings )
  13         31  
  13         21463  
62 64         395 while ( $spec =~ m/\G ( [[:alnum:]] ) ( ${ \( FICC ) } | ${ \( OAICC ) } | )/gcox ) {
  8         24  
  8         1694  
63 115         422 my ( $name, $indicator ) = ( $1, $2 );
64             croakf "%s parameter contains option '%s' multiple times", '$spec', $name
65 115 100       321 if exists $spec_as_hash->{ $name };
66 114         302 $spec_as_hash->{ $name } = $indicator;
67 114         530 ++$spec_length_got
68             }
69 63         129 my $offset = pos $spec;
70 63 100 100     371 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     247 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         196 $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 1111397 my ( $spec, $opts, $argv ) = @_;
81              
82 50 100       258 my $spec_as_hash = ref $spec eq 'HASH' ? $spec : parse_spec $spec;
83 44 100       131 croakf "%s parameter isn't an empty hash", '%$opts'
84             if %$opts;
85 43 100       139 $argv = \@ARGV unless defined $argv;
86              
87 43         163 my @argv_backup = @$argv;
88 43         86 my @error;
89             # Guideline 4, Guideline 9
90 43   100     397 while ( @$argv and my ( $name, $rest ) = ( $argv->[ 0 ] =~ m/\A ${ \( OD ) } (.) (.*)/ox ) ) {
  6         160  
91             # Guideline 10
92 123 100       332 shift @$argv, last
93             if $argv->[ 0 ] eq EOOD;
94             @error = ( 'illegal option', $name ), last
95 122 100       325 unless exists $spec_as_hash->{ $name }; ## no critic ( ProhibitNegativeExpressionsInUnlessAndUntilConditions )
96              
97 118         270 my $indicator = $spec_as_hash->{ $name };
98 118 100       314 if ( $indicator =~ m/\A ${ \( OAICC ) } \z/ox ) {
  5         153  
99             # Case: Option has an option-argument
100             # Shift delimeted option name
101 55         126 shift @$argv;
102             # Extract option-argument value
103 55         93 my $value;
104 55 100       122 if ( $rest ne '' ) {
105 6         47 $value = $rest;
106             } else {
107             # Guideline 7
108 49 100       119 @error = ( 'option requires an argument', $name ), last
109             unless @$argv;
110             # Guideline 6, Guideline 8
111 47 100       133 @error = ( 'option requires an argument', $name ), last
112             unless defined( $value = shift @$argv );
113             }
114             # Store option-argument value
115 52 100       119 if ( $indicator eq ':' ) {
116             # Standard behaviour: Overwrite option-argument
117 31         231 $opts->{ $name } = $value
118             } else {
119             # Create and fill list of option-arguments ( $indicator eq ',' )
120 21 100       147 $opts->{ $name } = [] unless exists $opts->{ $name };
121 21         39 push @{ $opts->{ $name } }, $value
  21         195  
122             }
123             } else {
124             # Case: Option is a flag
125 63 100       249 if ( not exists $opts->{ $name } ) {
    100          
    100          
126             # Initialisation
127 45         121 $opts->{ $name } = TRUE
128             } elsif ( $indicator eq '!' ) {
129             # Negate logically
130 1         4 $opts->{ $name } = not $opts->{ $name }
131             } elsif ( $indicator eq '+' ) {
132             # Increment
133 14         32 ++$opts->{ $name }
134             }
135             # Guideline 5
136 63 100       152 if ( $rest eq '' ) {
137             # Shift delimeted option name
138 47         300 shift @$argv
139             } else {
140 16         136 $argv->[ 0 ] = OD . $rest ## no critic ( RequireLocalizedPunctuationVars )
141             }
142             }
143              
144             }
145              
146 43 100       133 if ( @error ) {
147             # Restore to avoid side effects
148 7         25 @$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         24 warn sprintf( "%s: %s -- %s\n", program_name(), @error ) ## no critic ( RequireCarping )
153             }
154              
155 43         781 @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 197726 printf STDOUT "%s %s\nperl v%vd\n", program_name(), ( caller( 1 ) // 'main' )->VERSION, $^V;
167 3         193 EOOD
168             }
169              
170             sub processopts ( \@@ ) {
171 17     17 0 854852 my $argv = shift;
172 17         45 my $spec_as_array = do { my $t = 0; [ grep $t ^= 1, @_ ] }; ## no critic ( RequireBlockGrep )
  17         69  
  17         100  
173              
174             # Check each option specification individually (1)
175 17         34 my $spec_as_hash;
176 17         92 parse_spec $_, %$spec_as_hash, 1 for @$spec_as_array;
177              
178 15 100       72 return FALSE unless getopts $spec_as_hash, my %opts, @$argv;
179              
180             # This ordered processing is a feature!
181 13         56 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         24928 my ( $name, $indicator ) = split //, $_[ $i ];
185 18 100       79 if ( exists $opts{ $name } ) {
186 16         40 my $value = delete $opts{ $name };
187 16         42 my $dest = $_[ $i + 1 ];
188 16         52 my $dest_ref_type = ref $dest;
189 16 100 100     116 if ( $dest_ref_type eq 'SCALAR' ) {
    100          
    100          
190 4         8 ${ $dest } = $value
  4         16  
191             } elsif ( $dest_ref_type eq 'ARRAY' and $indicator eq ',' ) {
192 1         4 @{ $dest } = @$value
  1         7  
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     45 return ( OD . $name )
200             if ( $dest->( $value, $name, $indicator ) // '' ) eq EOOD
201             } else {
202 1         4 croakf "'%s' is an unsupported destination reference type for the '%s' indicator", $dest_ref_type, $indicator
203             }
204             }
205             }
206              
207 8         12692 TRUE
208             }
209              
210             # https://metacpan.org/pod/Getopt::ArgvFile
211             # https://pasdoc.github.io/ConfigFileOption
212             sub readopts ( \@ ) {
213 2     2 0 153636 my ( $argv ) = @_;
214              
215 2         10 require File::Spec::Functions;
216 2 100       6 return unless -f ( my $file = File::Spec::Functions::catfile( $ENV{ XDG_CONFIG_HOME }, program_name() . 'rc' ) );
217              
218 1 50   1   41 open my $fh, '<:encoding(UTF-8)', $file ## no critic ( RequireBriefOpen )
  1         597  
  1         14  
  1         5  
219             or croakf "Cannot open file '%s' for reading (%s)", $file, $!;
220              
221 1         970 while ( <$fh> ) {
222 2         14 chomp;
223 2 50       10 next if m/\A (?: \#.* | ) \z/x;
224 2 50       9 if ( m/\A ( [[:alnum:]] ) (?: \ ( .+ ) )? \z/x ) {
225 2 100       10 unshift @$argv, ( OD . $1, defined $2 ? $2 : () );
226             next
227 2         10 }
228 0         0 croakf "File '%s' contains the invalid line '%s'", $file, $_
229             }
230              
231             undef
232 1         15 }
233              
234             1