File Coverage

blib/lib/Devel/Bug.pm
Criterion Covered Total %
statement 142 149 95.3
branch 60 66 90.9
condition 43 58 74.1
subroutine 27 31 87.1
pod 1 2 50.0
total 273 306 89.2


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.06';
10              
11 5     5   1898022 use v5.8;
  5         14  
12 5     5   32 use utf8;
  5         17  
  5         35  
13              
14 5     5   164 use strict;
  5         10  
  5         137  
15 5     5   17 use warnings;
  5         7  
  5         347  
16              
17 5     5   3749 use Term::ANSIColor;
  5         45910  
  5         502  
18 5     5   48 use Carp qw(croak carp);
  5         10  
  5         836  
19              
20              
21 5         605 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   32 };
  5         8  
39              
40             use constant USE_OPTIONS => {
41 5         8 %{ +BUG_OPTIONS },
  5         875  
42             bug => [ '' ],
43 5     5   35 };
  5         14  
44              
45 5         9 use constant OPTION_ALIASES => do {
46 5         10 my (%h, $opt, $spec);
47 5         5 @h{ @{$spec}[1..$#$spec] }= ( $opt ) x $#$spec while ($opt, $spec)= each %{ +USE_OPTIONS };
  90         228  
  85         253  
48 5         379 \%h;
49 5     5   31 };
  5         7  
50              
51 5     5   29 use constant CALLER_INFO => qw(package filename lineno);
  5         6  
  5         4894  
52              
53              
54             # Terminal detection helpers. (Extracated from DESTROY to facilitate testing.)
55 41     41   161 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   12 my $w= eval { require Term::Size::Perl; (Term::Size::Perl::chars($_[0]))[0] };
  1         15  
  0         0  
60 1 50       268 $@ and carp qq(Unable to load Term::Size::Perl: specify option 'noterm => 1' to suppress this warning);
61 1 50       18 $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   264 sub _pairs { my $a= $_[0]; map [ $a->[ $_<<1 ], $a->[ ($_<<1) + 1 ] ], 0..$#$a>>1 }
  213         1649  
66              
67              
68             # Validate options according to provided definitions.
69             sub validate {
70 211     211 0 350 my $optDefs= shift;
71              
72             # Get label and option flags, if present.
73 211 100       536 my $label= @_ & 1? shift : '';
74 211   66     841 ($label, my $flags)= split /:/, defined($label) && $label, 2;
75              
76             unshift @_,
77             label => defined($label) && $label,
78 211   100     973 map { $_ => 1 } split //, defined($flags) && $flags;
  12   66     32  
79              
80             # Get key/value options.
81 211         303 my @options;
82              
83 211         346 foreach (_pairs \@_) {
84 809         1166 my $opt= lc $_->[0]; # option names are case insensitive
85 809         1074 local $_= $_->[1];
86              
87             # Convert an alias option name to its primary name.
88 809 100       1446 $opt= OPTION_ALIASES->{$opt} if exists OPTION_ALIASES->{$opt};
89              
90             # Confirm option name actually exists.
91 809 100       1614 exists $optDefs->{$opt} or croak qq(Unknown option '$opt');
92              
93             # Confirm and process option values and their types.
94 807         1134 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     2998 : ($type eq '*' or $optDefs->{$opt}[0] eq ref or croak qq(Option '$opt' may not be type '@{[ ref || '(SCALAR)' ]}'));
      33        
99              
100 807         1812 push @options, $opt => $_;
101             }
102              
103 209         1789 @options;
104             }
105              
106              
107             my %OPTIONS;
108              
109             sub import {
110 79     79   45779 shift;
111              
112 79         258 %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         200 my $bug= 'bug';
119              
120             # Caller may export bug() under a different name or suppress export.
121 77 100       171 if (exists $OPTIONS{bug}) {
122             # Don't export anything if explicity set to falsy.
123 4 100       17 $bug= $OPTIONS{bug} or return;
124              
125             # Export bug() under a different name.
126 2 100       107 $bug=~/^ (?: [a-z]\w* | _\w+ ) $/ix or croak qq(Illegal characters in 'bug' replacement subroutine name '$bug');
127 1         3 delete $OPTIONS{bug};
128             }
129              
130             # Export bug().
131 5     5   38 no strict 'refs';
  5         8  
  5         3270  
132 74         114 *{ (caller).'::'.$bug }= \&bug;
  74         427  
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 4851 my $self= bless { %OPTIONS, validate BUG_OPTIONS, @_ }, __PACKAGE__;
146              
147             # Get extra info to include with output.
148 53         105 my %info;
149              
150 53         256 @info{ (CALLER_INFO) }= caller;
151 53         171 $info{lineno}= "line $info{lineno}";
152              
153 53         368 $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 100       157 if (wantarray) {
160 7         13 $self->{data}= [];
161              
162 7         9 our @lvArray;
163 7         12 local @lvArray;
164 7         20 tie @lvArray, __PACKAGE__, $self;
165            
166 7         32 @lvArray;
167             } else {
168 46         88 $self->{data}= \my $scalar;
169              
170 46         55 our $lvScalar;
171 46         54 local $lvScalar;
172 46         202 tie $lvScalar, __PACKAGE__, $self;
173              
174 46         237 $lvScalar;
175             }
176             # Implicit return used to avoid known perl bug: "Bizarre copy of ARRAY in return" in some perl versions.
177             }
178              
179             # Just pass along self object constructed in bug(), which invokes these via tie().
180 46     46   115 sub TIESCALAR { $_[1] }
181 7     7   16 sub TIEARRAY { $_[1] }
182              
183             # Methods for tied array.
184 7     7   23 sub CLEAR { $_[0]->{data}= [] }
185 0     0   0 sub FETCHSIZE { @{ $_[0]->{data} } }
  0         0  
186              
187             # Shared methods: SCALAR ARRAY
188 66 100   66   147 sub FETCH { @_ == 1? ${ $_[0]->{data} } : $_[0]->{data}[ $_[1] ] }
  46         137  
189 66 100   66   152 sub STORE { @_ == 2? (${ $_[0]->{data} }= $_[1]) : ($_[0]->{data}[ $_[1] ]= $_[2]) }
  46         208  
190              
191             # Format and output captured values upon destruction of temporary tied variable.
192             sub DESTROY {
193 53     53   214 my $self= $_[0];
194 53         89 my $override= exists $self->{val};
195              
196             my ($data, $delims, $color, $multiline, $indices, $keyval, $ic, $lc, $vc)=
197 53         73 @{$self}{ qw(data delims color multiline indices keyval infocolor labelcolor valcolor) };
  53         192  
198              
199 53         98 my $isScalar= ref($data) eq 'SCALAR';
200              
201 53   100     219 $indices||= '';
202              
203             # Get terminal width if requested and available.
204 53 100 100     164 my $termW= (not $self->{noterm} and _isTerm($self->{out}))? _sttyWidth() || _tspWidth($self->{out}) : 0;
      100        
205              
206             # Get the pretty printer sub.
207 53         123 my $ppSub;
208              
209 53 100       102 if (defined $self->{pp}) {
210 7 100       9 eval {
211             # Caller specified sub in Module::sub form.
212 7         8 my $pp= $self->{pp};
213 7 100       190 my ($ppPN)= $pp=~/^(.+)::.+$/i or die qq(Invalid pretty-printer '$pp' specified: expected 'Module::sub' form);
214              
215 6         17 $ppPN=~s{::}{/}g; # get module path with slashes for require
216              
217             # Load the module.
218 6 100       13 eval { require "$ppPN.pm" } or die $@;
  6         986  
219              
220             # Confirm sub callable and save ref to it.
221 5     5   58 no strict 'refs';
  5         7  
  5         469  
222 5 100       6058 defined &$pp or die qq(Invalid pretty-printer '$pp');
223 4         13 $ppSub= \&$pp;
224             } or carp $@;
225             }
226              
227             # If no pretty printer specified or loading it didn't work, try the default.
228 5   66 5   31 $ppSub||= do { no warnings 'once'; require Data::Dumper; $Data::Dumper::Indent= 1; \&Data::Dumper::Dumper };
  5         11  
  5         4016  
  53         126  
  49         2237  
  49         21255  
  49         150  
229              
230             # Make a string representation of the data.
231             my $toString= sub {
232 61     61   90 my $color= $_[0]; # color the text with ANSI colors?
233 61 100 100     279 my $ml= $_[1] || $multiline || $indices? "\n" : ''; # multiline?
234              
235 61         105 my $label= $self->{label};
236 61         88 my $info= $self->{info};
237              
238 61         108 local $_;
239              
240             # Return a string representation of the value, coloring if needed.
241             my $cv= sub {
242 74 100       186 my $txt= ref $_[0]? $ppSub->($_[0]) : defined $_[0]? $_[0] : 'UNDEF';
    100          
243 74 100 66     924 ($color and $vc)? colored($txt, $vc) : $txt;
244 61         222 };
245              
246             # Make a string representation of the data.
247 61         82 my $i= 0;
248              
249             my $vals=
250             $ml.(
251             join $ml || ' ',
252 70 100       819 map { $ml? " $_" : $_ } # multiline?
253             $override? ( $cv->($self->{val}) ) # vals => ... override used
254             : $isScalar? ( $cv->($$data) ) # single scalar
255 4   66     11 : $keyval? ( map { ($indices && $i++.': ').$cv->($_->[0]).' => '.$cv->($_->[1]) } _pairs($data) ) # list of key/val pairs
256 61 100 100     269 : ( map { ($indices && $i++.': ').$cv->($_) } @$data ) # list
  12 100 66     62  
    100          
257             ).$ml;
258              
259             # Format info, label and vals, coloring if needed.
260 61 50 33     1348 $info= ($color and $ic)? colored($info, $ic).': ' : "$info: " if length $info;
    100          
261 61 100 66     233 $label= ($color and $lc)? colored($label, $lc).'=' : "$label=" if length $label;
    50          
262 61 100 66     749 $vals= '('.$vals.')' if $delims or defined($delims) and ($ml or not $color or length($vals) == 0);
      66        
      66        
263              
264 61         364 $info.$label.$vals;
265 53         466 };
266              
267             # Call toString once for possible sizing, then again if necessary for coloring and/or wrapping.
268 53         81 my $str;
269              
270 53   100     145 $str= $toString->($color and not $termW);
271 53 100       131 $str= $toString->(defined($color), $termW < length $str) if $termW;
272              
273             # Ouput the string.
274 53         74 print { $self->{out} } $str."\n";
  53         1294  
275             }
276              
277              
278             # Catch if user code attempts to use this like a regular object.
279             sub AUTOLOAD {
280 0     0     our $AUTOLOAD;
281 0           croak qq(Attempt to call unneeded non-existent subroutine '$AUTOLOAD': class @{[ __PACKAGE__ ]} intended for inline logging only);
  0            
282             }
283              
284             # If these get called, they need to be no-ops, since we're only using the tied array for logging.
285       6     sub EXTEND { }
286       0     sub STORESIZE { }
287              
288              
289              
290             1;
291              
292             __END__