File Coverage

blib/lib/Hub/Parse/Hash.pm
Criterion Covered Total %
statement 12 215 5.5
branch 0 136 0.0
condition 0 43 0.0
subroutine 4 13 30.7
pod 2 2 100.0
total 18 409 4.4


line stmt bran cond sub pod time code
1             package Hub::Parse::Hash;
2 1     1   5 use strict;
  1         2  
  1         30  
3 1     1   6 use Hub qw/:lib :console/;
  1         1  
  1         7  
4              
5             our $VERSION = '4.00043';
6             our @EXPORT = qw//;
7             our @EXPORT_OK = qw/
8             HASH_FORMAT_MAJOR_VERSION
9             HASH_FORMAT_MINOR_VERSION
10             hparse
11             hprint
12             /;
13              
14             # Version
15 1     1   6 use constant HASH_FORMAT_MAJOR_VERSION => 2;
  1         2  
  1         63  
16 1     1   6 use constant HASH_FORMAT_MINOR_VERSION => 1;
  1         2  
  1         4022  
17              
18             # Constants
19             our $NEWLINE = "\n";
20             our $SPACE = ' ';
21             our $INDENT = ' ';
22              
23             # Literal constants
24             our $LIT_OPEN = '{';
25             our $LIT_CLOSE = '}';
26             our $LIT_HASH = '%';
27             our $LIT_ARRAY = '@';
28             our $LIT_SCALAR = '$';
29             our $LIT_ASSIGN = '=>';
30             our $LIT_COMMENT = '#';
31             our $LIT_COMMENT_BEGIN = '#{';
32             our $LIT_COMMENT_END = '#}';
33              
34             # Used in regular expressions
35             our $PAT_OPEN = $LIT_OPEN;
36             our $PAT_CLOSE = $LIT_CLOSE;
37             our $PAT_HASH = $LIT_HASH;
38             our $PAT_ARRAY = $LIT_ARRAY;
39             our $PAT_SCALAR = "\\$LIT_SCALAR";
40             our $PAT_ASSIGN = $LIT_ASSIGN;
41             our $PAT_ASSIGN_STRUCT = '[\$\%\@]';
42             our $PAT_ASSIGN_BLOCK = '<<';
43             our $PAT_COMMENT = $LIT_COMMENT;
44             our $PAT_COMMENT_BEGIN = $LIT_COMMENT_BEGIN;
45             our $PAT_COMMENT_END = $LIT_COMMENT_END;
46             #our $PAT_LVAL = '[\w\d\.\_\-\s]';
47             our $PAT_LVAL = '[^\{\=]';
48             our $PAT_PROTECTED = '[\%\@\$\{\}\>\#]';
49             our $PAT_PROTECTED2 = '[\%\@\$\{\}\>\=\#]'; # backward compat
50             our $PAT_BLOCK_END = '[a-zA-Z0-9_-]';
51              
52             # ------------------------------------------------------------------------------
53             # hparse - Parse text into perl data structures
54             # hparse \$text, [options]
55             # options:
56             # -as_array=1 # Treat text as an array list (and return an array ref)
57             # -hint=hint # Usually a filename, used in debug/error output
58             # ------------------------------------------------------------------------------
59              
60             sub hparse {
61 0     0 1   my ($opts, $text) = Hub::opts(\@_, {
62             'hint' => '',
63             'as_array' => 0,
64             });
65 0 0         croak "Provide a scalar reference" unless ref($text) eq 'SCALAR';
66 0 0         my $root = $$opts{'into'} ? $$opts{'into'} : ();
67 0 0 0       $root ||= $$opts{'as_array'} ? [] : Hub::mkinst('SortedHash');
68 0           my $ptr = $root;
69 0           my $block_comment = 0;
70 0           my $block_text = 0;
71 0           my @parents = ();
72 0           local $. = 0;
73              
74 0           for (split /\r?\n\r?/, $$text) {
75 0           $.++;
76              
77 0 0         if ($block_comment) {
78             # End of a block comment?
79 0 0         /\s*$PAT_COMMENT_END/ and do {
80 0 0         next if (ref($ptr) eq 'SCALAR');
81 0           _trace($., "comment-e", $_);
82 0           $block_comment = 0;
83 0           next;
84             };
85 0           _trace($., "comment+", $_);
86 0           next;
87             }
88              
89 0 0         if ($block_text) {
90             # End of a text block?
91 0 0         /\s*$block_text\s*/ and do {
92 0           _trace($., "txtblk-e", $_);
93 0           $block_text = 0;
94 0           $ptr = pop @parents;
95 0           next;
96             };
97 0           _trace($., "txtblk+", $_);
98 0 0         $$ptr .= $$ptr ? $NEWLINE . _unescape($_) : _unescape($_);
99 0           next;
100             }
101              
102             # Begin of a new hash structure
103 0 0         /^\s*$PAT_HASH($PAT_LVAL*)\s*$PAT_OPEN?\s*$/ and do {
104 0           _trace($., "hash", $_);
105 0           push @parents, $ptr;
106             # my %h; tie %h, 'Hub::Knots::SortedHash';
107 0           my $h = Hub::mkinst('SortedHash');
108 0           my $var_name = _trim_whitespace(\$1);
109 0 0         isa($ptr, 'HASH') and $ptr->{$var_name} = $h;
110 0 0         isa($ptr, 'ARRAY') and push @$ptr, $h;
111 0           $ptr = $h;
112 0           next;
113             };
114              
115             # Begin of a new array structure
116 0 0         /^\s*$PAT_ARRAY($PAT_LVAL*)\s*$PAT_OPEN?\s*$/ and do {
117 0           _trace($., "array", $_);
118 0           push @parents, $ptr;
119 0           my $a = [];
120 0           my $var_name = _trim_whitespace(\$1);
121 0 0         isa($ptr, 'HASH') and $ptr->{$var_name} = $a;
122 0 0         isa($ptr, 'ARRAY') and push @$ptr, $a;
123 0           $ptr = $a;
124 0           next;
125             };
126              
127             # Begin of a new scalar structure
128 0 0         /^\s*$PAT_SCALAR($PAT_LVAL*)\s*$PAT_OPEN?\s*$/ and do {
129 0           _trace($., "scalar", $_);
130 0           push @parents, $ptr;
131 0 0         if (isa($ptr, 'HASH')) {
    0          
132 0           my $var_name = _trim_whitespace(\$1);
133 0           $ptr->{$var_name} = '';
134 0           $ptr = \$ptr->{$var_name};
135             } elsif (isa($ptr, 'ARRAY')) {
136 0           push @$ptr, '';
137 0           $ptr = \$ptr->[$#$ptr];
138             }
139 0           next;
140             };
141              
142             # A one-line hash member value
143 0 0         /^\s*($PAT_LVAL+)\s*$PAT_ASSIGN\s*(.*)/ and do {
144 0           my $lval = $1;
145 0           my $rval = $2;
146 0           my $var_name = _trim_whitespace(\$lval);
147              
148             # Structure assignment
149 0 0         $rval =~ /($PAT_ASSIGN_STRUCT)\s*$PAT_OPEN?\s*$/ and do {
150 0           _trace($., "assign-$1", $_);
151 0 0         unless (isa($ptr, 'HASH')) {
152 0           warn "Cannot assign structure to '$ptr'",
153             _get_hint($., $_, $$opts{'hint'});
154 0           next;
155             }
156 0           push @parents, $ptr;
157 0 0         if ($1 eq $LIT_HASH) {
    0          
    0          
158 0           my $h = Hub::mkinst('SortedHash');
159 0           $ptr->{$var_name} = $h;
160 0           $ptr = $h;
161             } elsif ($1 eq $LIT_ARRAY) {
162 0           my $a = [];
163 0           $ptr->{$var_name} = $a;
164 0           $ptr = $a;
165             } elsif ($1 eq $LIT_SCALAR) {
166 0           $ptr->{$var_name} = '';
167 0           $ptr = \$ptr->{$var_name};
168             } else {
169 0           warn "Unexpected structure assignment",
170             _get_hint($., $_, $$opts{'hint'});
171             }
172 0           next;
173             };
174              
175             # Block assignment
176 0 0         $rval =~ /$PAT_ASSIGN_BLOCK\s*($PAT_BLOCK_END+)\s*$/ and do {
177 0           _trace($., "txtblk", $_);
178 0           push @parents, $ptr;
179 0 0         if (isa($ptr, 'HASH')) {
    0          
180 0           $ptr->{$var_name} = '';
181 0           $ptr = \$ptr->{$var_name};
182             } elsif (isa($ptr, 'ARRAY')) {
183 0           push @$ptr, '';
184 0           $ptr = \$ptr->[$#$ptr];
185             }
186 0           $block_text = $1;
187 0           next;
188             };
189              
190             # Value assignment
191 0           _trace($., "assign", $_);
192 0 0         unless (isa($ptr, 'HASH')) {
193 0           warn "Cannot assign variable to '$ptr'", _get_hint($., $_, $$opts{'hint'});
194 0 0         isa($ptr, 'ARRAY') and push @$ptr, $_;
195 0 0         isa($ptr, 'SCALAR') and $$ptr .= $_;
196 0           next;
197             }
198 0           $ptr->{$var_name} = $rval;
199 0           next;
200             };
201              
202             # Close a structure
203 0 0         /^\s*$PAT_CLOSE\s*$/ and do {
204 0           _trace($., "close", $_);
205 0           $ptr = pop @parents;
206 0 0         unless (defined $ptr) {
207 0           warn "No parent" . _get_hint($., $_, $$opts{'hint'});
208             }
209 0           next;
210             };
211              
212             # If this is a brand new structure then this could be a hanging brace.
213 0 0         /^\s*$PAT_OPEN\s*/ and do {
214 0 0 0       if ((isa($ptr, 'HASH') && !keys(%$ptr))
      0        
      0        
      0        
      0        
215             || (isa($ptr, 'ARRAY') && !@$ptr)
216             || (ref($ptr) eq 'SCALAR' && !$$ptr)) {
217 0           _trace($., "hanging", $_);
218 0           next;
219             }
220             };
221              
222             # A block comment
223 0 0         /^\s*$PAT_COMMENT_BEGIN/ and do {
224 0 0         next if (ref($ptr) eq 'SCALAR');
225 0           _trace($., "comment-b", $_);
226 0           $block_comment = 1;
227 0           next;
228             };
229              
230             # A one-line comment
231 0 0         /^\s*$PAT_COMMENT/ and do {
232 0 0         if ($. == 1) {
233 0           _trace($., "crown", $_);
234 0           my @parts = split '\s';
235 0 0 0       if (@parts >= 3 && $parts[0] =~ /^Hash(File|Format)$/) {
236 0           my ($major, $minor) = split '\.', $parts[2];
237 0 0         if ($major > HASH_FORMAT_MAJOR_VERSION) {
238 0           die "Hash format version '$major' is too new",
239             _get_hint($., $_, $$opts{'hint'});
240             }
241             }
242             } else {
243 0           _trace($., "comment", $_);
244             }
245 0 0         next unless (ref($ptr) eq 'SCALAR');
246             };
247              
248             # A one-line array item
249 0 0         ref($ptr) eq 'ARRAY' and do {
250 0           _trace($., "array+", $_);
251 0           s/^\s+//g;
252 0 0         next unless $_; # Could be a blank line (arrays of hashes)
253 0           push @$ptr, $_;
254 0           next;
255             };
256              
257             # Part of a scalar
258 0 0         ref($ptr) eq 'SCALAR' and do {
259 0           _trace($., "scalar+", $_);
260 0 0         $$ptr .= $$ptr ? $NEWLINE . _unescape($_) : _unescape($_);
261             # $$ptr .= $$ptr ? $NEWLINE . $_ : $_;
262 0           next;
263             };
264              
265 0           _trace($., "?", $_);
266             }
267              
268 0 0         warn "Unclosed structure" . _get_hint($., 'EOF', $$opts{'hint'}) if @parents > 1;
269 0           return $root;
270             }
271              
272             # ------------------------------------------------------------------------------
273             # hprint - Format nested data structure as string
274             # hprint [options]
275             #
276             # options:
277             #
278             # -as_ref => 1 Return a reference (default 0)
279             # ------------------------------------------------------------------------------
280              
281             sub hprint {
282 0     0 1   my ($opts, $ref) = Hub::opts(\@_, {'as_ref' => 0});
283 0 0         croak "Provide a reference" unless ref($ref);
284 0           my $result = _hprint($ref);
285 0 0         return $$opts{'as_ref'} ? $result : ref($result) eq 'SCALAR' ? $$result : '';
    0          
286             }
287              
288             # ------------------------------------------------------------------------------
289             # _hprint - Implementation of hprint
290             # ------------------------------------------------------------------------------
291              
292             sub _hprint {
293 0 0   0     my $ref = shift or croak "Provide a reference";
294 0   0       my $name = shift || '';
295 0   0       my $level = shift || 0;
296 0           my $parent = shift;
297 0           my $result_str = '';
298 0           my $result = \$result_str;
299              
300             # Tame beastly names
301 0 0 0       if ($name && $name !~ /^$PAT_LVAL+$/) {
302 0           $name = Hub::safestr($name);
303             }
304              
305 0 0 0       if (isa($ref, 'HASH') || isa($ref, 'ARRAY')) {
    0          
306              
307             # Structure declaration and name
308 0 0         if ($level > 0) {
309 0 0         my $symbol = isa($ref, 'HASH') ? $LIT_HASH : $LIT_ARRAY;
310 0 0 0       if (defined $parent && isa($parent, 'HASH')) {
311 0           $$result .= _get_indent($level)
312             .$name.$SPACE.$LIT_ASSIGN.$SPACE.$symbol.$LIT_OPEN.$NEWLINE;
313             } else {
314 0           $$result .= _get_indent($level) .$symbol.$name.$LIT_OPEN.$NEWLINE;
315             }
316             }
317              
318             # Contents
319 0 0         if (isa($ref, 'HASH')) {
    0          
320 0           $level++;
321 0           for (keys %$ref) {
322 0 0         if (ref($$ref{$_})) {
323 0           $$result .= ${_hprint($$ref{$_}, $_, $level, $ref)};
  0            
324             } else {
325 0           $$result .= ${_hprint(\$$ref{$_}, $_, $level, $ref)};
  0            
326             }
327             }
328 0           $level--;
329             } elsif (isa($ref, 'ARRAY')) {
330 0           $level++;
331 0           for (@$ref) {
332 0           $$result .= ref($_) ?
333 0           ${_hprint($_, '', $level, $ref)} :
334 0 0         ${_hprint(\$_, '', $level, $ref)};
335             }
336 0           $level--;
337             }
338              
339             # Close the structure
340 0 0         $$result .= _get_indent($level) . $LIT_CLOSE.$NEWLINE
341             if $level > 0;
342              
343             } elsif (ref($ref) eq 'SCALAR') {
344              
345 0           my $value = $$ref;
346 0 0         $value = '' unless defined $value;
347              
348             # Scalar
349 0 0 0       if (index($value, "\n") > -1 || $value =~ /^\s+/) {
350 0           $$result .= _get_indent($level);
351 0 0 0       if (defined $parent && isa($parent, 'HASH')) {
352 0           $$result .= $name.$SPACE.$LIT_ASSIGN.$SPACE.$LIT_SCALAR.$LIT_OPEN.$NEWLINE;
353             } else {
354 0           $$result .= $LIT_SCALAR.$name.$LIT_OPEN.$NEWLINE;
355             }
356             # Write a scalar block to protect data
357 0           $$result .= _escape($value).$NEWLINE;
358 0           $$result .= _get_indent($level) .$LIT_CLOSE.$NEWLINE;
359             } else {
360             # One-line scalar (key/value)
361 0 0         if ($name) {
362 0           $$result .= _get_indent($level) .
363             $name.$SPACE.$LIT_ASSIGN.$SPACE.$value.$NEWLINE;
364             } else {
365 0           $$result .= _get_indent($level) .$value.$NEWLINE;
366             }
367             }
368              
369             } else {
370              
371             # Catch-all
372 0           $$result .= _get_indent($level) . $LIT_COMMENT.$SPACE;
373 0 0 0       $$result .= $name.$SPACE.$LIT_ASSIGN.$SPACE if (defined $name && $name);
374 0           $$result .= $ref.'('.ref($ref).')'.$NEWLINE;
375              
376             }
377 0           return $result;
378             }
379              
380             sub _trim_whitespace {
381 0     0     my $result = ${$_[0]};
  0            
382 0           $result =~ s/^\s+|\s+$//g;
383 0           return $result;
384             }
385              
386             # ------------------------------------------------------------------------------
387             # _escape - Esacape patterns which would be interpred as control characters
388             # ------------------------------------------------------------------------------
389              
390             sub _escape {
391 0     0     my $result = $_[0];
392 0           $result =~ s/(?
393 0           return $result;
394             }#_escape
395              
396             # ------------------------------------------------------------------------------
397             # _unescape - Remove protective backslashes
398             # ------------------------------------------------------------------------------
399              
400             sub _unescape {
401 0     0     my $result = $_[0];
402 0           $result =~ s/\\($PAT_PROTECTED2)/$1/g;
403 0           return $result;
404             }#_unescape
405              
406             # ------------------------------------------------------------------------------
407             # _get_indent - Get the indent for formatting nested sructures
408             # _get_indent $level
409             # ------------------------------------------------------------------------------
410              
411             sub _get_indent {
412 0     0     my $indent = $INDENT;
413 0 0         return $_[0] > 1 ? $indent x= ($_[0] - 1): '';
414             }
415              
416             # ------------------------------------------------------------------------------
417             # _trace - Debug output
418             # ------------------------------------------------------------------------------
419              
420 0     0     sub _trace {
421             # warn sprintf("%4d", $_[0]), ": ", Hub::fw(10, $_[1]), " $_[2]\n";
422             }
423              
424             # ------------------------------------------------------------------------------
425             # _get_hint - Context information for error messages
426             # _get_hint $line_num, $line_text
427             # ------------------------------------------------------------------------------
428              
429             sub _get_hint {
430 0     0     my $result = '';
431 0 0         if (defined $_[2]) {
432 0           $result = " ($_[2])";
433             }
434 0           my $str = substr($_[1], 0, 40);
435 0           $str =~ s/^\s+//g;
436 0           $result .= " at line $_[0]: '$str'";
437 0           return $result;
438             }
439              
440             # ------------------------------------------------------------------------------
441             1;
442              
443             __END__