File Coverage

lib/ChordPro/Utils.pm
Criterion Covered Total %
statement 127 185 68.6
branch 20 58 34.4
condition 8 22 36.3
subroutine 24 29 82.7
pod 0 17 0.0
total 179 311 57.5


line stmt bran cond sub pod time code
1             #! perl
2              
3             package ChordPro::Utils;
4              
5 81     81   1080 use v5.26;
  81         330  
6 81     81   1103 use utf8;
  81         230  
  81         502  
7 81     81   2015 use Carp;
  81         179  
  81         6847  
8 81     81   660 use feature qw( signatures );
  81         176  
  81         6947  
9 81     81   609 no warnings "experimental::signatures";
  81         222  
  81         3582  
10 81     81   548 use parent qw(Exporter);
  81         226  
  81         509  
11              
12             our @EXPORT;
13              
14             ################ Platforms ################
15              
16 81 50   81   13076 use constant MSWIN => $^O =~ /MSWin|Windows_NT/i ? 1 : 0;
  81         177  
  81         20100  
17              
18 0     0 0 0 sub is_msw () { MSWIN }
  0         0  
  0         0  
19 6     6 0 11 sub is_macos () { $^O =~ /darwin/ }
  6         11  
  6         25  
20              
21             push( @EXPORT, 'is_msw', 'is_macos' );
22              
23             ################ Filenames ################
24              
25 81     81   656 use File::Glob ( ":bsd_glob" );
  81         177  
  81         21110  
26 81     81   679 use File::Spec;
  81         190  
  81         9490  
27              
28             # Derived from Path::ExpandTilde.
29              
30 81 50       7331 use constant BSD_GLOB_FLAGS => GLOB_NOCHECK | GLOB_QUOTE | GLOB_TILDE | GLOB_ERR
31             # add GLOB_NOCASE as in File::Glob
32 81     81   653 | ($^O =~ m/\A(?:MSWin32|VMS|os2|dos|riscos)\z/ ? GLOB_NOCASE : 0);
  81         215  
33              
34             # File::Glob did not try %USERPROFILE% (set in Windows NT derivatives) for ~ before 5.16
35 81     81   717 use constant WINDOWS_USERPROFILE => MSWIN && $] < 5.016;
  81         228  
  81         122150  
36              
37 408     408 0 763 sub expand_tilde ( $dir ) {
  408         803  
  408         679  
38              
39 408 50       1113 return undef unless defined $dir;
40 408 50       3617 return File::Spec->canonpath($dir) unless $dir =~ m/^~/;
41              
42             # Parse path into segments.
43 0         0 my ( $volume, $directories, $file ) = File::Spec->splitpath( $dir, 1 );
44 0         0 my @parts = File::Spec->splitdir($directories);
45 0         0 my $first = shift( @parts );
46 0 0       0 return File::Spec->canonpath($dir) unless defined $first;
47              
48             # Expand first segment.
49 0         0 my $expanded;
50 0         0 if ( WINDOWS_USERPROFILE and $first eq '~' ) {
51             $expanded = $ENV{HOME} || $ENV{USERPROFILE};
52             }
53             else {
54 0         0 ( my $pattern = $first ) =~ s/([\\*?{[])/\\$1/g;
55 0         0 ($expanded) = bsd_glob( $pattern, BSD_GLOB_FLAGS );
56 0 0       0 croak( "Failed to expand $first: $!") if GLOB_ERROR;
57             }
58 0 0 0     0 return File::Spec->canonpath($dir)
59             if !defined $expanded or $expanded eq $first;
60              
61             # Replace first segment with new path.
62 0         0 ( $volume, $directories ) = File::Spec->splitpath( $expanded, 1 );
63 0         0 $directories = File::Spec->catdir( $directories, @parts );
64 0         0 return File::Spec->catpath($volume, $directories, $file);
65             }
66              
67             push( @EXPORT, 'expand_tilde' );
68              
69 0     0 0 0 sub findexe ( $prog, $silent = 0 ) {
  0         0  
  0         0  
  0         0  
70 0         0 my @path;
71 0         0 if ( MSWIN ) {
72             $prog .= ".exe" unless $prog =~ /\.\w+$/;
73             @path = split( ';', $ENV{PATH} );
74             unshift( @path, '.' );
75             }
76             else {
77 0         0 @path = split( ':', $ENV{PATH} );
78             }
79 0         0 foreach ( @path ) {
80 0         0 my $try = "$_/$prog";
81 0 0       0 if ( -f -x $try ) {
82             #warn("Found $prog in $_\n");
83 0         0 return $try;
84             }
85             }
86             warn("Could not find $prog in ",
87 0 0       0 join(" ", map { qq{"$_"} } @path), "\n") unless $silent;
  0         0  
88 0         0 return;
89             }
90              
91             push( @EXPORT, 'findexe' );
92              
93 0     0 0 0 sub sys ( @cmd ) {
  0         0  
  0         0  
94 0 0       0 warn("+ @cmd\n") if $::options->{trace};
95             # Use outer defined subroutine, depends on Wx or not.
96 0         0 my $res = ::sys(@cmd);
97 0 0       0 warn( sprintf("=%02x=> @cmd", $res), "\n" ) if $res;
98 0         0 return $res;
99             }
100              
101             push( @EXPORT, 'sys' );
102              
103             ################ (Pre)Processing ################
104              
105 170     170 0 352 sub make_preprocessor ( $prp ) {
  170         426  
  170         381  
106 170 50       568 return unless $prp;
107              
108 170         376 my $prep;
109 170         378 foreach my $linetype ( keys %{ $prp } ) {
  170         1113  
110 510         3679 my @targets;
111 510         890 my $code = "";
112 510         741 foreach ( @{ $prp->{$linetype} } ) {
  510         1114  
113 0   0     0 my $flags = $_->{flags} // "g";
114             $code .= "m\0" . $_->{select} . "\0 && "
115 0 0       0 if $_->{select};
116 0 0       0 if ( $_->{pattern} ) {
117             $code .= "s\0" . $_->{pattern} . "\0"
118 0         0 . $_->{replace} . "\0$flags;\n";
119             }
120             else {
121             $code .= "s\0" . quotemeta($_->{target}) . "\0"
122 0         0 . quotemeta($_->{replace}) . "\0$flags;\n";
123             }
124             }
125 510 50       1296 if ( $code ) {
126 0         0 my $t = "sub { for (\$_[0]) {\n" . $code . "}}";
127 0         0 $prep->{$linetype} = eval $t;
128 0 0       0 die( "CODE : $t\n$@" ) if $@;
129             }
130             }
131 170         649 $prep;
132             }
133              
134             push( @EXPORT, 'make_preprocessor' );
135              
136             ################ Utilities ################
137              
138             # Split (pseudo) command line into key/value pairs.
139              
140 6     6 0 1190 sub parse_kv ( @lines ) {
  6         18  
  6         10  
141              
142 6 50       15 if ( is_macos() ) {
143             # MacOS has the nasty habit to smartify quotes.
144 0         0 @lines = map { s/“/"/g; s/”/"/g; s/‘/'/g; s/’/'/gr;} @lines;
  0         0  
  0         0  
  0         0  
  0         0  
145             }
146              
147 81     81   44190 use Text::ParseWords qw(shellwords);
  81         116199  
  81         124935  
148 6         24 my @words = shellwords(@lines);
149              
150 6         1063 my $res = {};
151 6         18 foreach ( @words ) {
152 22 100       86 if ( /^(.*?)=(.+)/ ) {
    100          
153 14         48 $res->{$1} = $2;
154             }
155             elsif ( /^no[-_]?(.+)/ ) {
156 2         8 $res->{$1} = 0;
157             }
158             else {
159 6         17 $res->{$_}++;
160             }
161             }
162              
163 6         31 return $res;
164             }
165              
166             push( @EXPORT, 'parse_kv' );
167              
168             # Map true/false etc to true / false.
169              
170 449     449 0 711 sub is_true ( $arg ) {
  449         710  
  449         587  
171 449 50 33     1702 return if !defined($arg) || $arg eq '';
172 449 100       1874 return if $arg =~ /^(false|null|no|none|off|\s+|0)$/i;
173 436         1535 return !!$arg;
174             }
175              
176             push( @EXPORT, 'is_true' );
177              
178             # Stricter form of true.
179 9     9 0 17 sub is_ttrue ( $arg ) {
  9         19  
  9         20  
180 9 50       23 return if !defined($arg);
181 9         63 $arg =~ /^(on|true|1)$/i;
182             }
183              
184             push( @EXPORT, 'is_ttrue' );
185              
186             # Fix apos -> quote.
187              
188 1179     1179 0 1673 sub fq ( $arg ) {
  1179         1826  
  1179         1593  
189 1179         2103 $arg =~ s/'/\x{2019}/g;
190 1179         3864 $arg;
191             }
192              
193             push( @EXPORT, 'fq' );
194              
195             # Quote a string if needed unless forced.
196              
197 11     11 0 20 sub qquote ( $arg, $force = 0 ) {
  11         24  
  11         21  
  11         18  
198 11         26 for ( $arg ) {
199 11         31 s/([\\\"])/\\$1/g;
200 11         31 s/([[:^print:]])/sprintf("\\u%04x", ord($1))/ge;
  0         0  
201 11 100 100     64 return $_ unless /[\\\s]/ || $force;
202 9         40 return qq("$_");
203             }
204             }
205              
206             push( @EXPORT, 'qquote' );
207              
208             # Turn foo.bar.blech=blah into { foo => { bar => { blech ==> "blah" } } }.
209              
210 119     119 0 327 sub prp2cfg ( $defs, $cfg ) {
  119         311  
  119         293  
  119         304  
211 119         360 my $ccfg = {};
212 119   50     1593 $cfg //= {};
213 119         894 while ( my ($k, $v) = each(%$defs) ) {
214 10         77 my @k = split( /[:.]/, $k );
215 10         24 my $c = \$ccfg; # new
216 10         26 my $o = $cfg; # current
217 10         27 my $lk = pop(@k); # last key
218              
219             # Step through the keys.
220 10         29 foreach ( @k ) {
221 13         38 $c = \($$c->{$_});
222 13         34 $o = $o->{$_};
223             }
224              
225             # Final key. Merge array if so.
226 10 50 33     63 if ( $lk =~ /^\d+$/ && ref($o) eq 'ARRAY' ) {
227 0 0       0 unless ( ref($$c) eq 'ARRAY' ) {
228             # Only copy orig values the first time.
229 0         0 $$c->[$_] = $o->[$_] for 0..scalar(@{$o})-1;
  0         0  
230             }
231 0         0 $$c->[$lk] = $v;
232             }
233             else {
234 10         68 $$c->{$lk} = $v;
235             }
236             }
237 119         522 return $ccfg;
238             }
239              
240             push( @EXPORT, 'prp2cfg' );
241              
242             # Remove markup.
243 2254     2254 0 18468 sub demarkup ( $t ) {
  2254         3502  
  2254         2858  
244 2254         4298 return join( '', grep { ! /^\
  2275         10302  
245             }
246             push( @EXPORT, 'demarkup' );
247              
248             # Split into markup/nonmarkup segments.
249 2276     2276 0 3048 sub splitmarkup ( $t ) {
  2276         3274  
  2276         2898  
250 2276         11764 my @t = split( qr;();, $t );
251 2276         7121 return @t;
252             }
253             push( @EXPORT, 'splitmarkup' );
254              
255             # For conditional filling of hashes.
256 46     46 0 68 sub maybe ( $key, $value, @rest ) {
  46         81  
  46         66  
  46         74  
  46         62  
257 46 50 33     196 if (defined $key and defined $value) {
258 0         0 return ( $key, $value, @rest );
259             }
260             else {
261 46 50 33     346 ( defined($key) || @rest ) ? @rest : ();
262             }
263             }
264             push( @EXPORT, "maybe" );
265              
266             # Min/Max.
267 0 0   0 0   sub min { $_[0] < $_[1] ? $_[0] : $_[1] }
268 0 0   0 0   sub max { $_[0] > $_[1] ? $_[0] : $_[1] }
269              
270             push( @EXPORT, "min", "max" );
271              
272             1;