File Coverage

blib/lib/Data/Dump/IfSmall.pm
Criterion Covered Total %
statement 15 351 4.2
branch 0 212 0.0
condition 0 93 0.0
subroutine 5 17 29.4
pod 0 10 0.0
total 20 683 2.9


line stmt bran cond sub pod time code
1             ## no critic: Modules::ProhibitAutomaticExportation
2             package Data::Dump::IfSmall;
3              
4 1     1   323039 use strict 'vars', 'subs';
  1         2  
  1         62  
5 1     1   7 use Exporter qw(import);
  1         2  
  1         120  
6              
7             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
8             our $DATE = '2024-02-16'; # DATE
9             our $DIST = 'Data-Dump-IfSmall'; # DIST
10             our $VERSION = '0.001'; # VERSION
11              
12             our @EXPORT = qw(dd ddx);
13             our @EXPORT_OK = qw(dump pp dumpf quote);
14              
15 1     1   12 use overload ();
  1         2  
  1         38  
16 1     1   6 use vars qw(%seen %refcnt @dump @fixup %require $MAX_SIZE $DEBUG $SHOW_FIXUPS $TRY_BASE64 @FILTERS $INDENT $SORT_KEYS $REMOVE_PRAGMAS);
  1         3  
  1         2678  
17              
18             our $MAX_SIZE = $ENV{PERL_DATA_DUMP_IFSMALL_MAX_SIZE} || 1024;
19              
20             $DEBUG = 0;
21              
22             $SHOW_FIXUPS = 1;
23             $TRY_BASE64 = 50 unless defined $TRY_BASE64;
24             $INDENT = " " unless defined $INDENT;
25              
26             $SORT_KEYS = undef;
27              
28             $REMOVE_PRAGMAS = 0;
29              
30             sub dump
31             {
32 0     0 0   local %seen;
33 0           local %refcnt;
34 0           local %require;
35 0           local @fixup;
36              
37 0 0         require Data::Dump::FilterContext if @FILTERS;
38              
39 0           my $name = "a";
40 0           my @dump;
41              
42 0           for my $v (@_) {
43 0           my $val = _dump($v, $name, [], tied($v));
44 0           push(@dump, [$name, $val]);
45             } continue {
46 0           $name++;
47             }
48              
49 0           my $out = "";
50 0 0         if (%require) {
51 0           for (sort keys %require) {
52 0           $out .= "require $_;\n";
53             }
54             }
55 0 0         if (%refcnt) {
56             # output all those with refcounts first
57 0           for (@dump) {
58 0           my $name = $_->[0];
59 0 0         if ($refcnt{$name}) {
60 0           $out .= "my \$$name = $_->[1];\n";
61 0           undef $_->[1];
62             }
63             }
64 0 0         if ($SHOW_FIXUPS) {
65 0           for (@fixup) {
66 0           $out .= "$_;\n";
67             }
68             }
69             }
70              
71 0           my $paren = (@dump != 1);
72 0 0         $out .= "(" if $paren;
73             $out .= format_list($paren, undef,
74 0 0         map {defined($_->[1]) ? $_->[1] : "\$".$_->[0]}
  0            
75             @dump
76             );
77 0 0         $out .= ")" if $paren;
78              
79 0 0 0       if (%refcnt || %require) {
80 0           $out .= ";\n";
81 0           $out =~ s/^/$INDENT/gm;
82 0           $out = "do {\n$out}";
83             }
84              
85 0 0 0       if (length($out) > $MAX_SIZE && ((@_ == 1 && ref($_[0])) || (@_ > 1))) {
      0        
86 0 0         $out = @_ == 1 ? "'LARGE:$_[0]'" : "'LARGE:" . \@_ . "'";
87             }
88              
89 0 0         print STDERR "$out\n" unless defined wantarray;
90 0           $out;
91             }
92              
93             *pp = \&dump;
94              
95             sub dd {
96 0     0 0   print &dump(@_), "\n";
97             }
98              
99             sub ddx {
100 0     0 0   my(undef, $file, $line) = caller;
101 0           $file =~ s,.*[\\/],,;
102 0           my $out = "$file:$line: " . &dump(@_) . "\n";
103 0           $out =~ s/^/# /gm;
104 0           print $out;
105             }
106              
107             sub dumpf {
108 0     0 0   require Data::Dump::Filtered;
109 0           goto &Data::Dump::Filtered::dump_filtered;
110             }
111              
112             sub _dump
113             {
114 0     0     my $ref = ref $_[0];
115 0 0         my $rval = $ref ? $_[0] : \$_[0];
116 0           shift;
117              
118 0           my($name, $idx, $dont_remember, $pclass, $pidx) = @_;
119              
120 0           my($class, $type, $id);
121 0           my $strval = overload::StrVal($rval);
122             # Parse $strval without using regexps, in order not to clobber $1, $2,...
123 0 0         if ((my $i = rindex($strval, "=")) >= 0) {
124 0           $class = substr($strval, 0, $i);
125 0           $strval = substr($strval, $i+1);
126             }
127 0 0         if ((my $i = index($strval, "(0x")) >= 0) {
128 0           $type = substr($strval, 0, $i);
129 0           $id = substr($strval, $i + 2, -1);
130             }
131             else {
132 0           die "Can't parse " . overload::StrVal($rval);
133             }
134 0 0 0       if ($] < 5.008 && $type eq "SCALAR") {
135 0 0         $type = "REF" if $ref eq "REF";
136             }
137 0 0         warn "\$$name(@$idx) $class $type $id ($ref)" if $DEBUG;
138              
139 0           my $out;
140             my $comment;
141 0           my $hide_keys;
142 0 0         if (@FILTERS) {
143 0           my $pself = "";
144 0 0         $pself = fullname("self", [@$idx[$pidx..(@$idx - 1)]]) if $pclass;
145 0           my $ctx = Data::Dump::FilterContext->new($rval, $class, $type, $ref, $pclass, $pidx, $idx);
146 0           my @bless;
147 0           for my $filter (@FILTERS) {
148 0 0         if (my $f = $filter->($ctx, $rval)) {
149 0 0         if (my $v = $f->{object}) {
150 0           local @FILTERS;
151 0           $out = _dump($v, $name, $idx, 1);
152 0           $dont_remember++;
153             }
154 0 0         if (defined(my $c = $f->{bless})) {
155 0           push(@bless, $c);
156             }
157 0 0         if (my $c = $f->{comment}) {
158 0           $comment = $c;
159             }
160 0 0         if (defined(my $c = $f->{dump})) {
161 0           $out = $c;
162 0           $dont_remember++;
163             }
164 0 0         if (my $h = $f->{hide_keys}) {
165 0 0         if (ref($h) eq "ARRAY") {
166             $hide_keys = sub {
167 0     0     for my $k (@$h) {
168 0 0         return 1 if $k eq $_[0];
169             }
170 0           return 0;
171 0           };
172             }
173             }
174             }
175             }
176 0 0 0       push(@bless, "") if defined($out) && !@bless;
177 0 0         if (@bless) {
178 0           $class = shift(@bless);
179 0 0         warn "More than one filter callback tried to bless object" if @bless;
180             }
181             }
182              
183 0 0         unless ($dont_remember) {
184 0 0         if (my $s = $seen{$id}) {
185 0           my($sname, $sidx) = @$s;
186 0           $refcnt{$sname}++;
187 0   0       my $sref = fullname($sname, $sidx,
188             ($ref && $type eq "SCALAR"));
189 0 0         warn "SEEN: [\$$name(@$idx)] => [\$$sname(@$sidx)] ($ref,$sref)" if $DEBUG;
190 0 0         return $sref unless $sname eq $name;
191 0           $refcnt{$name}++;
192 0           push(@fixup, fullname($name,$idx)." = $sref");
193 0 0 0       return "do{my \$fix}" if @$idx && $idx->[-1] eq '$';
194 0           return "'fix'";
195             }
196 0           $seen{$id} = [$name, $idx];
197             }
198              
199 0 0         if ($class) {
200 0           $pclass = $class;
201 0           $pidx = @$idx;
202             }
203              
204 0 0 0       if (defined $out) {
    0 0        
    0          
    0          
    0          
    0          
    0          
205             # keep it
206             }
207             elsif ($type eq "SCALAR" || $type eq "REF" || $type eq "REGEXP") {
208 0 0         if ($ref) {
209 0 0 0       if ($class && $class eq "Regexp") {
210 0           my $v = "$rval";
211              
212 0           my $mod = "";
213 0 0         if ($v =~ /^\(\?\^?([msix-]*):([\x00-\xFF]*)\)\z/) {
214 0           $mod = $1;
215 0           $v = $2;
216 0           $mod =~ s/-.*//;
217             }
218              
219 0           my $sep = '/';
220 0           my $sep_count = ($v =~ tr/\///);
221 0 0         if ($sep_count) {
222             # see if we can find a better one
223 0           for ('|', ',', ':', '#') {
224 0           my $c = eval "\$v =~ tr/\Q$_\E//";
225             #print "SEP $_ $c $sep_count\n";
226 0 0         if ($c < $sep_count) {
227 0           $sep = $_;
228 0           $sep_count = $c;
229 0 0         last if $sep_count == 0;
230             }
231             }
232             }
233 0           $v =~ s/\Q$sep\E/\\$sep/g;
234              
235 0           $out = "qr$sep$v$sep$mod";
236 0           undef($class);
237             }
238             else {
239 0 0         delete $seen{$id} if $type eq "SCALAR"; # will be seen again shortly
240 0           my $val = _dump($$rval, $name, [@$idx, "\$"], 0, $pclass, $pidx);
241 0 0         $out = $class ? "do{\\(my \$o = $val)}" : "\\$val";
242             }
243             } else {
244 0 0         if (!defined $$rval) {
    0          
245 0           $out = "undef";
246             }
247 1     1   10 elsif (do {no warnings 'numeric'; $$rval + 0 eq $$rval}) {
  1         2  
  1         5073  
  0            
248 0           $out = $$rval;
249             }
250             else {
251 0           $out = str($$rval);
252             }
253 0 0 0       if ($class && !@$idx) {
254             # Top is an object, not a reference to one as perl needs
255 0           $refcnt{$name}++;
256 0           my $obj = fullname($name, $idx);
257 0           my $cl = quote($class);
258 0           push(@fixup, "bless \\$obj, $cl");
259             }
260             }
261             }
262             elsif ($type eq "GLOB") {
263 0 0         if ($ref) {
264 0           delete $seen{$id};
265 0           my $val = _dump($$rval, $name, [@$idx, "*"], 0, $pclass, $pidx);
266 0           $out = "\\$val";
267 0 0         if ($out =~ /^\\\*Symbol::/) {
268 0           $require{Symbol}++;
269 0           $out = "Symbol::gensym()";
270             }
271             } else {
272 0           my $val = "$$rval";
273 0           $out = "$$rval";
274              
275 0           for my $k (qw(SCALAR ARRAY HASH)) {
276 0           my $gval = *$$rval{$k};
277 0 0         next unless defined $gval;
278 0 0 0       next if $k eq "SCALAR" && ! defined $$gval; # always there
279 0           my $f = scalar @fixup;
280 0           push(@fixup, "RESERVED"); # overwritten after _dump() below
281 0           $gval = _dump($gval, $name, [@$idx, "*{$k}"], 0, $pclass, $pidx);
282 0           $refcnt{$name}++;
283 0           my $gname = fullname($name, $idx);
284 0           $fixup[$f] = "$gname = $gval"; #XXX indent $gval
285             }
286             }
287             }
288             elsif ($type eq "ARRAY") {
289 0           my @vals;
290 0           my $tied = tied_str(tied(@$rval));
291 0           my $i = 0;
292 0           for my $v (@$rval) {
293 0           push(@vals, _dump($v, $name, [@$idx, "[$i]"], $tied, $pclass, $pidx));
294 0           $i++;
295             }
296 0           $out = "[" . format_list(1, $tied, @vals) . "]";
297             }
298             elsif ($type eq "HASH") {
299 0           my(@keys, @vals);
300 0           my $tied = tied_str(tied(%$rval));
301              
302             # statistics to determine variation in key lengths
303 0           my $kstat_max = 0;
304 0           my $kstat_sum = 0;
305 0           my $kstat_sum2 = 0;
306              
307 0           my @orig_keys = keys %$rval;
308 0 0         if ($hide_keys) {
309 0           @orig_keys = grep !$hide_keys->($_), @orig_keys;
310             }
311 0 0         if (defined $SORT_KEYS) {
312 0           @orig_keys = $SORT_KEYS->($rval);
313             }
314             else {
315 0           my $text_keys = 0;
316 0           for (@orig_keys) {
317 0 0         $text_keys++, last unless /^[-+]?(?:0|[1-9]\d*)(?:\.\d+)?\z/;
318             }
319              
320 0 0         if ($text_keys) {
321 0           @orig_keys = sort { lc($a) cmp lc($b) } @orig_keys;
  0            
322             }
323             else {
324 0           @orig_keys = sort { $a <=> $b } @orig_keys;
  0            
325             }
326             }
327              
328 0           my $quote;
329 0           for my $key (@orig_keys) {
330 0 0         next if $key =~ /^-?[a-zA-Z_]\w*\z/;
331 0 0         next if $key =~ /^-?[1-9]\d{0,8}\z/;
332 0           $quote++;
333 0           last;
334             }
335              
336 0           for my $key (@orig_keys) {
337 0           my $val = \$rval->{$key}; # capture value before we modify $key
338 0 0         $key = quote($key) if $quote;
339 0 0         $kstat_max = length($key) if length($key) > $kstat_max;
340 0           $kstat_sum += length($key);
341 0           $kstat_sum2 += length($key)*length($key);
342              
343 0           push(@keys, $key);
344 0           push(@vals, _dump($$val, $name, [@$idx, "{$key}"], $tied, $pclass, $pidx));
345             }
346 0           my $nl = "";
347 0           my $klen_pad = 0;
348 0           my $tmp = "@keys @vals";
349 0 0 0       if (length($tmp) > 60 || $tmp =~ /\n/ || $tied) {
      0        
350 0           $nl = "\n";
351              
352             # Determine what padding to add
353 0 0         if ($kstat_max < 4) {
    0          
354 0           $klen_pad = $kstat_max;
355             }
356             elsif (@keys >= 2) {
357 0           my $n = @keys;
358 0           my $avg = $kstat_sum/$n;
359 0           my $stddev = sqrt(($kstat_sum2 - $n * $avg * $avg) / ($n - 1));
360              
361             # I am not actually very happy with this heuristics
362 0 0         if ($stddev / $kstat_max < 0.25) {
363 0           $klen_pad = $kstat_max;
364             }
365 0 0         if ($DEBUG) {
366 0           push(@keys, "__S");
367 0           push(@vals, sprintf("%.2f (%d/%.1f/%.1f)",
368             $stddev / $kstat_max,
369             $kstat_max, $avg, $stddev));
370             }
371             }
372             }
373 0           $out = "{$nl";
374 0 0         $out .= "$INDENT# $tied$nl" if $tied;
375 0           while (@keys) {
376 0           my $key = shift @keys;
377 0           my $val = shift @vals;
378 0 0         my $vpad = $INDENT . (" " x ($klen_pad ? $klen_pad + 4 : 0));
379 0           $val =~ s/\n/\n$vpad/gm;
380 0 0         my $kpad = $nl ? $INDENT : " ";
381 0 0 0       $key .= " " x ($klen_pad - length($key)) if $nl && $klen_pad > length($key);
382 0           $out .= "$kpad$key => $val,$nl";
383             }
384 0 0         $out =~ s/,$/ / unless $nl;
385 0           $out .= "}";
386             }
387             elsif ($type eq "CODE") {
388 0           $out = code($rval);
389             }
390             elsif ($type eq "VSTRING") {
391 0 0         $out = sprintf +($ref ? '\v%vd' : 'v%vd'), $$rval;
392             }
393             else {
394 0           warn "Can't handle $type data";
395 0           $out = "'#$type#'";
396             }
397              
398 0 0 0       if ($class && $ref) {
399 0           $out = "bless($out, " . quote($class) . ")";
400             }
401 0 0         if ($comment) {
402 0           $comment =~ s/^/# /gm;
403 0 0         $comment .= "\n" unless $comment =~ /\n\z/;
404 0           $comment =~ s/^#[ \t]+\n/\n/;
405 0           $out = "$comment$out";
406             }
407 0           return $out;
408             }
409              
410             sub tied_str {
411 0     0 0   my $tied = shift;
412 0 0         if ($tied) {
413 0 0         if (my $tied_ref = ref($tied)) {
414 0           $tied = "tied $tied_ref";
415             }
416             else {
417 0           $tied = "tied";
418             }
419             }
420 0           return $tied;
421             }
422              
423             sub fullname
424             {
425 0     0 0   my($name, $idx, $ref) = @_;
426 0           substr($name, 0, 0) = "\$";
427              
428 0           my @i = @$idx; # need copy in order to not modify @$idx
429 0 0 0       if ($ref && @i && $i[0] eq "\$") {
      0        
430 0           shift(@i); # remove one deref
431 0           $ref = 0;
432             }
433 0   0       while (@i && $i[0] eq "\$") {
434 0           shift @i;
435 0           $name = "\$$name";
436             }
437              
438 0           my $last_was_index;
439 0           for my $i (@i) {
440 0 0 0       if ($i eq "*" || $i eq "\$") {
    0          
441 0           $last_was_index = 0;
442 0           $name = "$i\{$name}";
443             } elsif ($i =~ s/^\*//) {
444 0           $name .= $i;
445 0           $last_was_index++;
446             } else {
447 0 0         $name .= "->" unless $last_was_index++;
448 0           $name .= $i;
449             }
450             }
451 0 0         $name = "\\$name" if $ref;
452 0           $name;
453             }
454              
455             sub format_list
456             {
457 0     0 0   my $paren = shift;
458 0           my $comment = shift;
459 0 0         my $indent_lim = $paren ? 0 : 1;
460 0 0         if (@_ > 3) {
461             # can we use range operator to shorten the list?
462 0           my $i = 0;
463 0           while ($i < @_) {
464 0           my $j = $i + 1;
465 0           my $v = $_[$i];
466 0           while ($j < @_) {
467             # XXX allow string increment too?
468 0 0 0       if ($v eq "0" || $v =~ /^-?[1-9]\d{0,9}\z/) {
    0          
469 0           $v++;
470             }
471             elsif ($v =~ /^"([A-Za-z]{1,3}\d*)"\z/) {
472 0           $v = $1;
473 0           $v++;
474 0           $v = qq("$v");
475             }
476             else {
477 0           last;
478             }
479 0 0         last if $_[$j] ne $v;
480 0           $j++;
481             }
482 0 0         if ($j - $i > 3) {
483 0           splice(@_, $i, $j - $i, "$_[$i] .. $_[$j-1]");
484             }
485 0           $i++;
486             }
487             }
488 0           my $tmp = "@_";
489 0 0 0       if ($comment || (@_ > $indent_lim && (length($tmp) > 60 || $tmp =~ /\n/))) {
      0        
      0        
490 0           my @elem = @_;
491 0           for (@elem) { s/^/$INDENT/gm; }
  0            
492 0 0         return "\n" . ($comment ? "$INDENT# $comment\n" : "") .
493             join(",\n", @elem, "");
494             } else {
495 0           return join(", ", @_);
496             }
497             }
498              
499             my $deparse;
500             sub code {
501 0     0 0   my $code = shift;
502 0 0         unless ($deparse) {
503 0           require B::Deparse;
504 0           $deparse = B::Deparse->new("-l"); # -i option doesn't have any effect?
505             }
506              
507 0           my $res = $deparse->coderef2text($code);
508              
509 0           my ($res_before_first_line, $res_after_first_line) =
510             $res =~ /(.+?)^(#line .+)/ms;
511              
512 0 0         if ($REMOVE_PRAGMAS) {
513 0           $res_before_first_line = "{\n";
514             #} elsif ($PERL_VERSION < 5.016) {
515             # # older perls' feature.pm doesn't yet support q{no feature ':all';}
516             # # so we replace it with q{no feature}.
517             # $res_before_first_line =~ s/no feature ':all';/no feature;/m;
518             }
519 0           $res_after_first_line =~ s/^#line .+\n//gm;
520              
521 0           $res = "sub " . $res_before_first_line . $res_after_first_line;
522              
523 0 0         if (length($res) <= 60) {
524 0           $res =~ s/^ +//gm;
525 0           $res =~ s/\n+/ /g;
526 0           $res =~ s/;\s+\}\z/ }/;
527             } else {
528 0           $res =~ s/^ +/$INDENT/gm;
529             }
530              
531 0           $res;
532             }
533              
534             sub str {
535 0 0   0 0   if (length($_[0]) > 20) {
536 0           for ($_[0]) {
537             # Check for repeated string
538 0 0         if (/^(.)\1\1\1/s) {
539             # seems to be a repeating sequence, let's check if it really is
540             # without backtracking
541 0 0         unless (/[^\Q$1\E]/) {
542 0           my $base = quote($1);
543 0           my $repeat = length;
544 0           return "($base x $repeat)"
545             }
546             }
547             # Length protection because the RE engine will blow the stack [RT#33520]
548 0 0 0       if (length($_) < 16 * 1024 && /^(.{2,5}?)\1*\z/s) {
549 0           my $base = quote($1);
550 0           my $repeat = length($_)/length($1);
551 0           return "($base x $repeat)";
552             }
553             }
554             }
555              
556 0           local $_ = "e;
557              
558 0 0 0       if (length($_) > 40 && !/\\x\{/ && length($_) > (length($_[0]) * 2)) {
      0        
559             # too much binary data, better to represent as a hex/base64 string
560              
561             # Base64 is more compact than hex when string is longer than
562             # 17 bytes (not counting any require statement needed).
563             # But on the other hand, hex is much more readable.
564 0 0 0       if ($TRY_BASE64 && length($_[0]) > $TRY_BASE64 &&
      0        
      0        
      0        
565             (defined &utf8::is_utf8 && !utf8::is_utf8($_[0])) &&
566 0           eval { require MIME::Base64 })
567             {
568 0           $require{"MIME::Base64"}++;
569 0           return "MIME::Base64::decode(\"" .
570             MIME::Base64::encode($_[0],"") .
571             "\")";
572             }
573 0           return "pack(\"H*\",\"" . unpack("H*", $_[0]) . "\")";
574             }
575              
576 0           return $_;
577             }
578              
579             my %esc = (
580             "\a" => "\\a",
581             "\b" => "\\b",
582             "\t" => "\\t",
583             "\n" => "\\n",
584             "\f" => "\\f",
585             "\r" => "\\r",
586             "\e" => "\\e",
587             );
588              
589             # put a string value in double quotes
590             sub quote {
591 0     0 0   local($_) = $_[0];
592             # If there are many '"' we might want to use qq() instead
593 0           s/([\\\"\@\$])/\\$1/g;
594 0 0         return qq("$_") unless /[^\040-\176]/; # fast exit
595              
596 0           s/([\a\b\t\n\f\r\e])/$esc{$1}/g;
597              
598             # no need for 3 digits in escape for these
599 0           s/([\0-\037])(?!\d)/sprintf('\\%o',ord($1))/eg;
  0            
600              
601 0           s/([\0-\037\177-\377])/sprintf('\\x%02X',ord($1))/eg;
  0            
602 0           s/([^\040-\176])/sprintf('\\x{%X}',ord($1))/eg;
  0            
603              
604 0           return qq("$_");
605             }
606              
607             1;
608             # ABSTRACT: Like Data::Dump but reference with dump larger than a certain size will be dumped as something like 'LARGE:ARRAY(0x5636145ea5e8)'
609              
610             __END__