File Coverage

blib/lib/Config/Std.pm
Criterion Covered Total %
statement 243 271 89.6
branch 71 96 73.9
condition 32 61 52.4
subroutine 29 36 80.5
pod n/a
total 375 464 80.8


line stmt bran cond sub pod time code
1             package Config::Std;
2              
3             our $VERSION = '0.901';
4              
5             require v5.7.3; # RT#21184
6              
7             my %global_def_sep;
8             my %global_inter_gap;
9              
10             sub import {
11 9     9   89 my ($package, $opt_ref) = @_;
12 9         21 my $caller = caller();
13 9         31 $global_def_sep{$caller} = $opt_ref->{def_sep};
14 9         24 $global_inter_gap{$caller} = $opt_ref->{def_gap};
15 9         21 for my $sub_name (qw( read_config write_config )) {
16 18   66     174 $opt_ref->{$sub_name} ||= $sub_name;
17             }
18 9         26 *{$caller.'::'.$opt_ref->{read_config}} = \&Config::Std::Hash::read_config;
  9         67  
19 9         22 *{$caller.'::'.$opt_ref->{write_config}} = \&Config::Std::Hash::write_config;
  9         1677  
20             }
21              
22             package Config::Std::Gap;
23 10     10   135372 use Class::Std;
  10         210014  
  10         77  
24             {
25 39     39   79 sub serialize { return "\n" }
26 26     26   75 sub update {}
27 0     0   0 sub extend {}
28 42     42   63 sub copy_to {}
29             }
30              
31             package Config::Std::Comment;
32 10     10   2320 use Class::Std;
  10         21  
  10         43  
33             {
34             my %text_of : ATTR( :init_arg );
35              
36             sub serialize {
37 0     0   0 my ($self) = @_;
38 0         0 return $text_of{ident $self};
39             }
40              
41             sub append_comment {
42 0     0   0 my ($self, $new_text) = @_;
43 0         0 $text_of{ident $self} .= $new_text;
44             }
45              
46 0     0   0 sub update {}
47 0     0   0 sub extend {}
48 0     0   0 sub copy_to {}
49             }
50              
51             package Config::Std::Keyval;
52 10     10   2855 use Class::Std;
  10         27  
  10         49  
53             {
54             my %key_of : ATTR( :init_arg :get );
55             my %vals_of : ATTR;
56             my %deleted_of : ATTR;
57              
58             sub BUILD {
59 53     53   1851 my ($self, $ident, $arg_ref) = @_;
60              
61 53         67 $vals_of{$ident} = [ { %{$arg_ref} } ];
  53         373  
62             }
63              
64             my %SEPARATOR = ( ':' => ': ', '=' => ' = ' );
65              
66 10     10   2049 use Carp;
  10         20  
  10         8791  
67              
68             sub serialize {
69 36     36   50 my ($self, $def_sep, $block_name) = @_;
70 36         74 my $ident = ident $self;
71              
72 36 50       90 return "" if $deleted_of{$ident};
73              
74 36         72 my ($key, $vals) = ($key_of{$ident}, $vals_of{$ident});
75              
76 36         65 my $keyspace = q{ } x length($key);
77              
78 36         42 my $serialization = q{};
79              
80 36         46 for $n (0..$#{$vals}) {
  36         80  
81 47         53 my ($val,$sep,$comm) = @{$vals->[$n]}{qw(val sep comm)};
  47         332  
82              
83 47         56 my $val_type = ref $val;
84 47 50 66     136 croak qq{Can't save \L$val_type\E ref as value for key {'$block_name'}{'$key'} (only scalars or array refs)}
85             if $val_type && $val_type ne 'ARRAY';
86              
87 47   66     123 $sep = $SEPARATOR{$sep || $def_sep};
88              
89 47 100       118 my @vals = $val_type eq 'ARRAY' ? @{$val} : $val;
  1         4  
90 47         151 s/ (?!\Z) \n /\n$keyspace$sep/gxms for @vals;
91              
92 47   100     184 $serialization .= $comm || q{};
93              
94 47         69 $serialization .= join q{}, map {"$key$sep$_\n"} @vals;
  51         226  
95             }
96              
97 36         113 return $serialization;
98             }
99              
100             sub update {
101 25     25   35 my ($self, $hash_ref, $updated_ref) = @_;
102 25         51 my $ident = ident $self;
103              
104 25         46 my $key = $key_of{$ident};
105              
106 25 50       58 if (!exists $hash_ref->{$key}) {
107 0         0 $deleted_of{$ident} = 1;
108             }
109             else {
110 25         38 my $val = $hash_ref->{$key};
111 25 100       76 @newvals = ref $val eq 'ARRAY' ? @{$val} : $val;
  2         20  
112 25         57 for my $n (0..$#newvals) {
113 36         108 $vals_of{$ident}[$n]{val} = $newvals[$n];
114             }
115 25         37 splice @{$vals_of{$ident}}, scalar @newvals;
  25         68  
116             }
117              
118 25         48 $updated_ref->{$key} = 1;
119              
120 25         84 return 1;
121             }
122              
123             sub copy_to {
124 41     41   54 my ($self, $hash_ref) = @_;
125 41         80 my $ident = ident $self;
126 41         49 my @vals = map $_->{val}, @{$vals_of{$ident}};
  41         173  
127 41 100       176 $hash_ref->{$key_of{$ident}} = @vals > 1 ? \@vals : $vals[0];
128             }
129              
130             sub multivalue {
131 17     17   32 my ($self, $sep, $val, $comm) = @_;
132 17         19 push @{$vals_of{ident $self}}, {val=>$val, sep=>$sep, comm=>$comm};
  17         100  
133             }
134             }
135              
136             package Config::Std::Block;
137 10     10   70 use Class::Std;
  10         29  
  10         61  
138             {
139             my %name_of : ATTR( :init_arg :get default => '' );
140             my %sep_count_of : ATTR;
141             my %precomm_of : ATTR( :init_arg default => '' );
142             my %parcomm_of : ATTR( :init_arg default => '' );
143             my %components_of : ATTR;
144             my %deleted_of : ATTR;
145             my %seen : ATTR;
146             my %is_first : ATTR( :init_arg default => '' );
147              
148             sub BUILD {
149 24     24   1077 my ($self, $ident) = @_;
150 24         39 @{$sep_count_of{$ident}}{':','='} = (0,0);
  24         2453  
151 24         61 $components_of{$ident} = [];
152 24         81 $seen{$ident} = {};
153             }
154              
155             sub copy_to {
156 18     18   28 my ($self, $hash_ref) = @_;
157 18         38 my $ident = ident $self;
158              
159 18   50     112 my $keyvals = $hash_ref->{$name_of{$ident}} ||= {};
160              
161 18         25 for my $comp ( @{$components_of{$ident}} ) {
  18         39  
162 83         181 $comp->copy_to($keyvals);
163             }
164              
165 18         59 $hash_ref->{$name_of{$ident}} = $keyvals;
166             }
167              
168             sub serialize {
169 18     18   33 my ($self, $first, $caller, $post_gap, $inter_gap) = @_;
170 18         37 my $ident = ident $self;
171              
172 18 100       53 return q{} if $deleted_of{$ident};
173              
174 16   66     66 my $is_anon = $first && length($name_of{$ident}) == 0;
175              
176 16         23 my $serialization = q{};
177 16 100       121 if (!$is_anon) {
178 11 100 50     82 $serialization = ($precomm_of{$ident} || q{})
179             . "[$name_of{$ident}]"
180             . (defined $parcomm_of{$ident}?$parcomm_of{$ident}:q{})
181             . "\n";
182             }
183              
184 16         25 my $gds = $global_def_sep{$caller};
185 16 50       62 my $def_sep
    100          
186             = defined $gds ? $gds
187             : $sep_count_of{$ident}{':'} >= $sep_count_of{$ident}{'='} ? ':'
188             : '='
189             ;
190              
191 16 50 33     42 $self->ensure_gap() if $inter_gap && !$is_anon;
192              
193 16         19 for my $comp ( @{$components_of{$ident}} ) {
  16         34  
194 75         192 $serialization .= $comp->serialize($def_sep, $name_of{$ident});
195             }
196              
197 16         103 return $serialization;
198             }
199              
200             sub update {
201 13     13   25 my ($self, $hash_ref, $updated_ref) = @_;
202 13         27 my $ident = ident $self;
203              
204 13 100       35 if (!defined $hash_ref) {
205 2         3 $deleted_of{$ident} = 1;
206 2         6 return;
207             }
208              
209 11         16 for my $comp ( @{$components_of{$ident}} ) {
  11         226  
210 51 100       121 $comp->update($hash_ref, $updated_ref) or next;
211             }
212             }
213              
214             sub extend {
215 13     13   40 my ($self, $hash_ref, $updated_ref, $post_gap, $inter_gap) = @_;
216              
217             # Only the first occurrence of a block has new keys added...
218 13 50       46 return unless $is_first{ident $self};
219              
220 13         20 my $first = 1;
221             # RT 85956
222 13         15 for my $key ( sort grep {!$updated_ref->{$_}} keys %{$hash_ref}) {
  25         83  
  13         42  
223 0         0 my $value = $hash_ref->{$key};
224 0   0     0 my $separate = ref $value || $value =~ m/\n./xms;
225 0 0 0     0 $self->ensure_gap() if ($first ? $post_gap : $inter_gap)
    0          
226             || $separate;
227 0         0 $self->add_keyval($key, undef, $hash_ref->{$key});
228 0 0       0 $self->add_gap() if $separate;
229 0         0 $first = 0;
230             }
231             }
232              
233             sub ensure_gap {
234 13     13   18 my ($self) = @_;
235 13         42 my $comp_ref = $components_of{ident $self};
236 13 100 100     17 return if @{$comp_ref} && $comp_ref->[-1]->isa('Config::Std::Gap');
  13         106  
237 12         17 push @{$comp_ref}, Config::Std::Gap->new();
  12         52  
238             }
239              
240             sub add_gap {
241 44     44   140 my ($self) = @_;
242 44         47 push @{$components_of{ident $self}}, Config::Std::Gap->new();
  44         222  
243             }
244              
245             sub add_comment {
246 0     0   0 my ($self, $text) = @_;
247 0         0 my $comp_ref = $components_of{ident $self};
248 0 0 0     0 if ($comp_ref && @{$comp_ref} && $comp_ref->[-1]->isa('Config::Std::Comment') ) {
  0   0     0  
249 0         0 $comp_ref->[-1]->append_comment($text);
250             }
251             else {
252 0         0 push @{$comp_ref}, Config::Std::Comment->new({text=>$text});
  0         0  
253             }
254             }
255              
256             sub add_keyval {
257 70     70   213 my ($self, $key, $sep, $val, $comm) = @_;
258 70         224 my $ident = ident $self;
259              
260 70 100       208 $sep_count_of{$ident}{$sep}++ if $sep;
261              
262 70         108 my $seen = $seen{$ident};
263              
264 70 100       191 if ($seen->{$key}) {
265 17         62 $seen->{$key}->multivalue($sep, $val, $comm);
266 17         35 return;
267             }
268              
269 53         373 my $keyval
270             = Config::Std::Keyval->new({key=>$key, sep=>$sep, val=>$val, comm=>$comm});
271 53         2288 push @{$components_of{$ident}}, $keyval;
  53         103  
272 53         145 $seen->{$key} = $keyval;
273             }
274             }
275              
276             package Config::Std::Hash;
277 10     10   14764 use Class::Std;
  10         50  
  10         62  
278             {
279              
280 10     10   1242 use Carp;
  10         20  
  10         797  
281 10     10   56 use Fcntl ':flock'; # import LOCK_* constants
  10         17  
  10         34987  
282              
283             my %post_section_gap_for :ATTR;
284             my %array_rep_for :ATTR;
285             my %filename_for :ATTR;
286              
287             sub write_config (\[%$];$) {
288 8     8   1169 my ($hash_ref, $filename) = @_;
289 8 100       42 $hash_ref = ${$hash_ref} if ref $hash_ref eq 'REF';
  1         2  
290              
291 8 100       40 $filename = $filename_for{$hash_ref} if @_<2;
292              
293 8 50       26 croak "Missing filename for call to write_config()"
294             unless $filename;
295              
296 8         21 my $caller = caller;
297              
298 8 50       40 my $inter_gap
299             = exists $global_inter_gap{$caller} ? $global_inter_gap{$caller}
300             : 1;
301 8   66     55 my $post_gap
302             = $post_section_gap_for{$hash_ref}
303             || (defined $global_inter_gap{$caller} ? $global_inter_gap{$caller}
304             : 1
305             );
306              
307             # Update existing keyvals in each block...
308 8         14 my %updated;
309 8         12 for my $block ( @{$array_rep_for{$hash_ref}} ) {
  8         26  
310 13         47 my $block_name = $block->get_name();
311 13   50     154 $block->update($hash_ref->{$block_name}, $updated{$block_name}||={});
312             }
313              
314             # Add new keyvals to the first section of block...
315 8         15 for my $block ( @{$array_rep_for{$hash_ref}} ) {
  8         27  
316 13         85 my $block_name = $block->get_name();
317 13         123 $block->extend($hash_ref->{$block_name}, $updated{$block_name},
318             $post_gap, inter_gap
319             );
320             }
321              
322             # Add new blocks at the end...
323 8         18 for my $block_name ( sort grep {!$updated{$_}} keys %{$hash_ref} ) {
  17         48  
  8         27  
324 6         54 my $block = Config::Std::Block->new({name=>$block_name});
325 6         1388 my $subhash = $hash_ref->{$block_name};
326 6         14 my $first = 1;
327             # RT 85956
328 6         10 for my $key ( sort keys %{$subhash} ) {
  6         27  
329 13 100       39 if (!defined $subhash->{$key}) {
330 1         197 croak "Can't save undefined value for key {'$block_name'}{'$key'} (only scalars or array refs)";
331             }
332 12         18 my $value = $subhash->{$key};
333 12   100     62 my $separate = ref $value || $value =~ m/\n./xms;
334 12 100 100     82 $block->ensure_gap() if ($first ? $post_gap : $inter_gap)
    100          
335             || $separate;
336 12         440 $block->add_keyval($key, undef, $value);
337 12 100       30 $block->add_gap() if $separate;
338 12         105 $first = 0;
339             }
340 5         14 $block->ensure_gap();
341 5         391 push @{$array_rep_for{$hash_ref}}, $block;
  5         21  
342             }
343              
344 7 50       702 open my $fh, '>', $filename
345             or croak "Can't open config file '$filename' for writing (\L$!\E)";
346              
347 7 100 33     88 flock($fh,LOCK_EX|LOCK_NB)
348             || croak "Can't write to locked config file '$filename'"
349             if ! ref $filename;
350              
351 7         12 my $first = 1;
352 7         13 for my $block ( @{$array_rep_for{$hash_ref}} ) {
  7         27  
353 18         43 print {$fh} $block->serialize($first, scalar caller, $post_gap);
  18         69  
354 18         39 $first = 0;
355             }
356              
357 7 100       240 flock($fh,LOCK_UN) if ! ref $filename;
358              
359 7         112 return 1;
360             }
361              
362             sub read_config ($\[%$]) {
363 7     7   1324 my ($filename, $var_ref, $opt_ref) = @_;
364 7   50     33 my $var_type = ref($var_ref) || q{};
365 7         13 my $hash_ref;
366 7 100 66     66 if ($var_type eq 'SCALAR' && !defined ${$var_ref} ) {
  1 50       4  
367 1         3 ${$var_ref} = $hash_ref = {};
  1         2  
368             }
369             elsif ($var_type eq 'HASH') {
370 6         12 $hash_ref = $var_ref;
371             }
372             else {
373 0         0 croak q{Scalar second argument to 'read_config' must be empty};
374             }
375              
376 7         45 bless $hash_ref, 'Config::Std::Hash';
377              
378 7         33 my $blocks = $array_rep_for{$hash_ref}
379             = _load_config_for($filename, $hash_ref);
380              
381 7         14 for my $block ( @{$blocks} ) {
  7         20  
382 18         48 $block->copy_to($hash_ref);
383             }
384              
385 7         21 $filename_for{$hash_ref} = $filename;
386              
387             # Remove initial empty section if no data...
388 7 100       12 if (!keys %{ $hash_ref->{q{}} }) {
  7         33  
389 2         4 delete $hash_ref->{q{}};
390             }
391              
392 7         44 return 1;
393             }
394              
395             sub _load_config_for {
396 7     7   17 my ($filename, $hash_ref) = @_;
397              
398 7 50   1   335 open my $fh, '<', $filename
  1         9  
  1         2  
  1         8  
399             or croak "Can't open config file '$filename' (\L$!\E)";
400 7 100 33     1458 flock($fh,LOCK_SH|LOCK_NB)
401             || croak "Can't read from locked config file '$filename'"
402             if !ref $filename;
403 7         8 my $text = do{local $/; <$fh>};
  7         28  
  7         293  
404 7 100       53 flock($fh,LOCK_UN) if !ref $filename;
405              
406 7         92 my @config_file = Config::Std::Block->new({ name=>q{}, first=>1 });
407 7         1404 my $comment = q{};
408 7         13 my %seen;
409              
410             # Start tracking whether section markers have gaps after them...
411 7         73 $post_section_gap_for{$hash_ref} = 0;
412              
413 7         19 for ($text) {
414 7         26 pos = 0;
415 7         37 while (pos() < length() ) {
416             # Gap...
417 113 100       883 if (m/\G (?: [^\S\n]* (?:\n|\z)+)/gcxms) {
    100          
    100          
    50          
418             ### Found gap
419 42 50       278 $config_file[-1]->add_comment($comment) if $comment;
420 42         106 $config_file[-1]->add_gap();
421 42         2570 $comment = q{};
422             }
423              
424             # Comment...
425             elsif (m/\G (\s* [#;] [^\n]* (?:\n|\z) )/gcxms) {
426             ### Found comment: $1
427 2         8 $comment .= $1;
428             }
429              
430             # Block...
431             elsif (m/\G ([^\S\n]*) [[] ( [^]\n]* ) []] ( ([^\S\n]*) [#;] [^\n]* )? [^\S\n]* (?:\n|\z)/gcxms) {
432 11         47 my ($pre, $name, $parcomm, $ws) = ($1, $2, $3, $4);
433             ### Found block: $name
434 11 50       28 if ($parcomm) {
435 0         0 $pre = 2 + length($pre) + length($name) + length($ws);
436 0 0       0 if (m/\G ( (?: \n? [ ]{$pre,} [#] [^\n]* )+ )/gcxms) {
437 0         0 $parcomm .= "\n$1";
438             }
439             }
440 11         93 push @config_file,
441             Config::Std::Block->new({
442             name => $name,
443             precomm => $comment,
444             parcomm => $parcomm,
445             first => !$seen{$name}++,
446             });
447 11         592 $comment = q{};
448              
449             # Check for trailing gap...
450 11 50       89 $post_section_gap_for{$hash_ref}
451             += m/\G (?= [^\S\n]* (?:\n|\z) )/xms ? +1 : -1;
452             }
453              
454             # Key/value...
455             elsif (m/\G [^\S\n]* ([^=:\n]+?) [^\S\n]* ([:=] [^\S\n]*) ([^\n]*) (?:\n|\z)/gcxms) {
456 58         184 my ($key, $sep, $val) = ($1, $2, $3);
457              
458 58         72 my $pure_sep = $sep;
459 58         290 $pure_sep =~ s/\s*//g;
460              
461             # Continuation lines...
462 58         115 my $continued = 0;
463 58   66     2549 while (m/\G [^\S\n]* \Q$sep\E ([^\n]*) (?:\n|\z) /gcxms
464             || m/\G [^\S\n]* \Q$pure_sep\E ([^\n]*) (?:\n|\z) /gcxms
465             ) {
466 8         22 $val .= "\n$1";
467 8         141 $continued = 1;
468             }
469              
470 58 100       557 $val =~ s/\A \s*|\s* \z//gxms if !$continued;
471              
472             ### Found kv: $key, $val
473              
474 58         173 $config_file[-1]->add_keyval($key, $pure_sep, $val,
475 58         182 $comment); $comment = q{}; }
476              
477             # Mystery...
478             else {
479 0         0 my ($problem) = m/\G ([^\n]{10,40}|.{10}) /gcxms;
480 0         0 die "Error in config file '$filename' near:\n\n\t$problem\n";
481             }
482             }
483             }
484              
485 7         161 return \@config_file;
486             }
487              
488             }
489              
490              
491             1; # Magic true value required at end of module
492             __END__