File Coverage

blib/lib/Data/Dump/SkipObjects.pm
Criterion Covered Total %
statement 15 353 4.2
branch 0 210 0.0
condition 0 87 0.0
subroutine 5 18 27.7
pod 0 10 0.0
total 20 678 2.9


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