File Coverage

blib/lib/Devel/Bug.pm
Criterion Covered Total %
statement 140 147 95.2
branch 60 66 90.9
condition 43 58 74.1
subroutine 27 31 87.1
pod 1 2 50.0
total 271 304 89.1


line stmt bran cond sub pod time code
1             # Devel::Bug - Transparent inline debugging probe (pure Perl)
2             #
3             # Copyright (C) 2026 Kevin Shea
4             # This library is free software; you can redistribute it and/or modify
5             # it under the same terms as Perl itself.
6              
7             package Devel::Bug;
8              
9             our $VERSION = '0.07';
10              
11 5     5   2060689 use v5.8;
  5         21  
12 5     5   46 use utf8;
  5         11  
  5         49  
13              
14 5     5   219 use strict;
  5         7  
  5         155  
15 5     5   24 use warnings;
  5         8  
  5         372  
16              
17 5     5   3755 use Term::ANSIColor;
  5         52015  
  5         482  
18 5     5   40 use Carp qw(croak carp);
  5         11  
  5         933  
19              
20              
21 5         685 use constant BUG_OPTIONS => {
22             label => [ '' ],
23             noterm => [ '', qw(n noterminal) ],
24             out => [ '*', qw(o output) ],
25             delims => [ '+', qw(d delimiters) ],
26             color => [ '+' ],
27             infocolor => [ '', qw(ic) ],
28             labelcolor => [ '', qw(lc) ],
29             valcolor => [ '', qw(vc valuecolor) ],
30             multiline => [ '', qw(m ml) ],
31             indices => [ '', qw(i @ index indexes) ],
32             keyval => [ '', qw(k kv %) ],
33             package => [ '', qw(p pkg) ],
34             filename => [ '', qw(f fn) ],
35             lineno => [ '', qw(l ln line) ],
36             val => [ '*', qw(v value override) ],
37             pp => [ '' ],
38 5     5   35 };
  5         11  
39              
40             use constant USE_OPTIONS => {
41 5         9 %{ +BUG_OPTIONS },
  5         782  
42             bug => [ '' ],
43 5     5   41 };
  5         16  
44              
45 5         9 use constant OPTION_ALIASES => do {
46 5         8 my (%h, $opt, $spec);
47 5         7 @h{ @{$spec}[1..$#$spec] }= ( $opt ) x $#$spec while ($opt, $spec)= each %{ +USE_OPTIONS };
  90         226  
  85         236  
48 5         388 \%h;
49 5     5   29 };
  5         15  
50              
51 5     5   29 use constant CALLER_INFO => qw(package filename lineno);
  5         4  
  5         5279  
52              
53              
54             # Terminal detection helpers. (Extracated from DESTROY to facilitate testing.)
55 41     41   160 sub _isTerm { -t $_[0] }
56 0 0   0   0 sub _sttyWidth { (qx(stty size 2>/dev/null)=~/^\d+\s+(\d+)/)[0] || 0 }
57              
58             sub _tspWidth {
59 1     1   48 my $w= eval { require Term::Size::Perl; (Term::Size::Perl::chars($_[0]))[0] };
  1         18  
  0         0  
60 1 50       233 $@ and carp qq(Unable to load Term::Size::Perl: specify option 'noterm => 1' to suppress this warning);
61 1 50       14 $w || 0;
62             }
63              
64             # Takes an ARRAY REF and returns a list of ARRAY refs of pairs of elements from it.
65 213     213   273 sub _pairs { my $a= $_[0]; map [ $a->[ $_<<1 ], $a->[ ($_<<1) + 1 ] ], 0..$#$a>>1 }
  213         1642  
66              
67              
68             # Validate options according to provided definitions.
69             sub validate {
70 211     211 0 376 my $optDefs= shift;
71              
72             # Get label and option flags, if present.
73 211 100       573 my $label= @_ & 1? shift : '';
74 211   66     936 ($label, my $flags)= split /:/, defined($label) && $label, 2;
75              
76             unshift @_,
77             label => defined($label) && $label,
78 211   100     1052 map { $_ => 1 } split //, defined($flags) && $flags;
  12   66     45  
79              
80             # Get key/value options.
81 211         289 my @options;
82              
83 211         398 foreach (_pairs \@_) {
84 809         1254 my $opt= lc $_->[0]; # option names are case insensitive
85 809         1225 local $_= $_->[1];
86              
87             # Convert an alias option name to its primary name.
88 809 100       1607 $opt= OPTION_ALIASES->{$opt} if exists OPTION_ALIASES->{$opt};
89              
90             # Confirm option name actually exists.
91 809 100       1947 exists $optDefs->{$opt} or croak qq(Unknown option '$opt');
92              
93             # Confirm and process option values and their types.
94 807         1236 my $type= $optDefs->{$opt}[0];
95              
96             $type eq '+'
97             ? (defined and $_= m{^(?:on|1)$}i? 1 : m{^(?:auto|)$}i? '' : m{^off$}i? undef : croak qq(Illegal option value: $opt => '$_'))
98 807 100 66     3024 : ($type eq '*' or $optDefs->{$opt}[0] eq ref or croak qq(Option '$opt' may not be type '@{[ ref || '(SCALAR)' ]}'));
      33        
99              
100 807         1855 push @options, $opt => $_;
101             }
102              
103 209         1890 @options;
104             }
105              
106              
107             my %OPTIONS;
108              
109             sub import {
110 79     79   43848 shift;
111              
112 79         292 %OPTIONS= (
113             validate(USE_OPTIONS, out => *STDERR, delims => 'auto', color => 'auto', lc => 'bold', vc => 'red on_grey23'), # defaults
114             validate(USE_OPTIONS, @_)
115             );
116              
117             # Default name under which to export bug().
118 77         227 my $bug= 'bug';
119              
120             # Caller may export bug() under a different name or suppress export.
121 77 100       172 if (exists $OPTIONS{bug}) {
122             # Don't export anything if explicity set to falsy.
123 4 100       10 $bug= $OPTIONS{bug} or return;
124              
125             # Export bug() under a different name.
126 2 100       163 $bug=~/^ (?: [a-z]\w* | _\w+ ) $/ix or croak qq(Illegal characters in 'bug' replacement subroutine name '$bug');
127 1         1 delete $OPTIONS{bug};
128             }
129              
130             # Export bug().
131 5     5   38 no strict 'refs';
  5         6  
  5         3002  
132 74         116 *{ (caller).'::'.$bug }= \&bug;
  74         432  
133             }
134              
135              
136             # Debugging utility class.
137             # Allows for inlining a temporary "bug" sub which will output intermediate expression data.
138             # Ex: my $infoPN= $CV_INFO_DIR."/".(bug('relpath')= substr($_, length($sourceDir) + 1));
139             # Output: relpath=(...)
140              
141             # To preserve list context, use form: (bug 'list')= ( some list );
142              
143             sub bug :lvalue {
144             # Create object and populate it with options from import and bug().
145 53     53 1 5970 my $self= bless { %OPTIONS, validate BUG_OPTIONS, @_ }, __PACKAGE__;
146              
147             # Get extra info to include with output.
148 53         118 my %info;
149              
150 53         256 @info{ (CALLER_INFO) }= caller;
151 53         155 $info{lineno}= "line $info{lineno}";
152              
153 53         301 $self->{info}= join(' ', map $info{$_}, grep $self->{$_}, CALLER_INFO);
154              
155             # Tie an array or scalar, for list or scalar context respectively.
156             # Package variables localized per call: named vars are never "temporaries" (avoids
157             # "Can't return a temporary from lvalue subroutine" on v5.8-5.12 and "Bizarre copy
158             # of ARRAY" on affected versions). local() is reentrant via per-call save-points.
159 53         78 our (@lvArray, $lvScalar);
160 53         98 local (@lvArray, $lvScalar);
161              
162 53 100       96 if (wantarray) {
163 7         18 $self->{data}= [];
164              
165 7         27 tie @lvArray, __PACKAGE__, $self;
166 7         40 @lvArray;
167             } else {
168 46         92 $self->{data}= \my $scalar;
169              
170 46         186 tie $lvScalar, __PACKAGE__, $self;
171 46         233 $lvScalar;
172             }
173             # Implicit return used to avoid known perl bug: "Bizarre copy of ARRAY in return" in some perl versions.
174             }
175              
176             # Just pass along self object constructed in bug(), which invokes these via tie().
177 46     46   121 sub TIESCALAR { $_[1] }
178 7     7   23 sub TIEARRAY { $_[1] }
179              
180             # Methods for tied array.
181 7     7   34 sub CLEAR { $_[0]->{data}= [] }
182 0     0   0 sub FETCHSIZE { @{ $_[0]->{data} } }
  0         0  
183              
184             # Shared methods: SCALAR ARRAY
185 66 100   66   131 sub FETCH { @_ == 1? ${ $_[0]->{data} } : $_[0]->{data}[ $_[1] ] }
  46         132  
186 66 100   66   166 sub STORE { @_ == 2? (${ $_[0]->{data} }= $_[1]) : ($_[0]->{data}[ $_[1] ]= $_[2]) }
  46         186  
187              
188             # Format and output captured values upon destruction of temporary tied variable.
189             sub DESTROY {
190 53     53   209 my $self= $_[0];
191 53         90 my $override= exists $self->{val};
192              
193             my ($data, $delims, $color, $multiline, $indices, $keyval, $ic, $lc, $vc)=
194 53         78 @{$self}{ qw(data delims color multiline indices keyval infocolor labelcolor valcolor) };
  53         208  
195              
196 53         99 my $isScalar= ref($data) eq 'SCALAR';
197              
198 53   100     204 $indices||= '';
199              
200             # Get terminal width if requested and available.
201 53 100 100     173 my $termW= (not $self->{noterm} and _isTerm($self->{out}))? _sttyWidth() || _tspWidth($self->{out}) : 0;
      100        
202              
203             # Get the pretty printer sub.
204 53         117 my $ppSub;
205              
206 53 100       118 if (defined $self->{pp}) {
207 7 100       13 eval {
208             # Caller specified sub in Module::sub form.
209 7         10 my $pp= $self->{pp};
210 7 100       214 my ($ppPN)= $pp=~/^(.+)::.+$/i or die qq(Invalid pretty-printer '$pp' specified: expected 'Module::sub' form);
211              
212 6         25 $ppPN=~s{::}{/}g; # get module path with slashes for require
213              
214             # Load the module.
215 6 100       8 eval { require "$ppPN.pm" } or die $@;
  6         1266  
216              
217             # Confirm sub callable and save ref to it.
218 5     5   37 no strict 'refs';
  5         8  
  5         520  
219 5 100       7724 defined &$pp or die qq(Invalid pretty-printer '$pp');
220 4         15 $ppSub= \&$pp;
221             } or carp $@;
222             }
223              
224             # If no pretty printer specified or loading it didn't work, try the default.
225 5   66 5   53 $ppSub||= do { no warnings 'once'; require Data::Dumper; $Data::Dumper::Indent= 1; \&Data::Dumper::Dumper };
  5         23  
  5         3884  
  53         132  
  49         2205  
  49         26094  
  49         137  
226              
227             # Make a string representation of the data.
228             my $toString= sub {
229 61     61   85 my $color= $_[0]; # color the text with ANSI colors?
230 61 100 100     275 my $ml= $_[1] || $multiline || $indices? "\n" : ''; # multiline?
231              
232 61         104 my $label= $self->{label};
233 61         120 my $info= $self->{info};
234              
235 61         93 local $_;
236              
237             # Return a string representation of the value, coloring if needed.
238             my $cv= sub {
239 74 100       179 my $txt= ref $_[0]? $ppSub->($_[0]) : defined $_[0]? $_[0] : 'UNDEF';
    100          
240 74 100 66     887 ($color and $vc)? colored($txt, $vc) : $txt;
241 61         215 };
242              
243             # Make a string representation of the data.
244 61         88 my $i= 0;
245              
246             my $vals=
247             $ml.(
248             join $ml || ' ',
249 70 100       722 map { $ml? " $_" : $_ } # multiline?
250             $override? ( $cv->($self->{val}) ) # vals => ... override used
251             : $isScalar? ( $cv->($$data) ) # single scalar
252 4   66     14 : $keyval? ( map { ($indices && $i++.': ').$cv->($_->[0]).' => '.$cv->($_->[1]) } _pairs($data) ) # list of key/val pairs
253 61 100 100     267 : ( map { ($indices && $i++.': ').$cv->($_) } @$data ) # list
  12 100 66     39  
    100          
254             ).$ml;
255              
256             # Format info, label and vals, coloring if needed.
257 61 50 33     163 $info= ($color and $ic)? colored($info, $ic).': ' : "$info: " if length $info;
    100          
258 61 100 66     265 $label= ($color and $lc)? colored($label, $lc).'=' : "$label=" if length $label;
    50          
259 61 100 66     659 $vals= '('.$vals.')' if $delims or defined($delims) and ($ml or not $color or length($vals) == 0);
      66        
      66        
260              
261 61         371 $info.$label.$vals;
262 53         424 };
263              
264             # Call toString once for possible sizing, then again if necessary for coloring and/or wrapping.
265 53         78 my $str;
266              
267 53   100     153 $str= $toString->($color and not $termW);
268 53 100       135 $str= $toString->(defined($color), $termW < length $str) if $termW;
269              
270             # Ouput the string.
271 53         82 print { $self->{out} } $str."\n";
  53         1356  
272             }
273              
274              
275             # Catch if user code attempts to use this like a regular object.
276             sub AUTOLOAD {
277 0     0     our $AUTOLOAD;
278 0           croak qq(Attempt to call unneeded non-existent subroutine '$AUTOLOAD': class @{[ __PACKAGE__ ]} intended for inline logging only);
  0            
279             }
280              
281             # If these get called, they need to be no-ops, since we're only using the tied array for logging.
282       6     sub EXTEND { }
283       0     sub STORESIZE { }
284              
285              
286              
287             1;
288              
289             __END__