File Coverage

blib/lib/Devel/Bug.pm
Criterion Covered Total %
statement 138 145 95.1
branch 60 66 90.9
condition 43 58 74.1
subroutine 27 31 87.1
pod 1 2 50.0
total 269 302 89.0


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.08';
10              
11 5     5   2096803 use v5.20;
  5         22  
12 5     5   31 use utf8;
  5         20  
  5         46  
13              
14 5     5   149 use strict;
  5         7  
  5         105  
15 5     5   17 use warnings;
  5         9  
  5         344  
16              
17 5     5   3140 use Term::ANSIColor;
  5         43695  
  5         514  
18 5     5   67 use Carp qw(croak carp);
  5         10  
  5         731  
19              
20              
21 5         584 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   29 };
  5         6  
39              
40             use constant USE_OPTIONS => {
41 5         12 %{ +BUG_OPTIONS },
  5         794  
42             bug => [ '' ],
43 5     5   55 };
  5         10  
44              
45 5         9 use constant OPTION_ALIASES => do {
46 5         11 my (%h, $opt, $spec);
47 5         8 @h{ @{$spec}[1..$#$spec] }= ( $opt ) x $#$spec while ($opt, $spec)= each %{ +USE_OPTIONS };
  90         244  
  85         273  
48 5         348 \%h;
49 5     5   34 };
  5         23  
50              
51 5     5   29 use constant CALLER_INFO => qw(package filename lineno);
  5         20  
  5         4627  
52              
53              
54             # Terminal detection helpers. (Extracated from DESTROY to facilitate testing.)
55 41     41   112 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   38 my $w= eval { require Term::Size::Perl; (Term::Size::Perl::chars($_[0]))[0] };
  1         11  
  0         0  
60 1 50       177 $@ and carp qq(Unable to load Term::Size::Perl: specify option 'noterm => 1' to suppress this warning);
61 1 50       9 $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   284 sub _pairs { my $a= $_[0]; map [ $a->[ $_<<1 ], $a->[ ($_<<1) + 1 ] ], 0..$#$a>>1 }
  213         1610  
66              
67              
68             # Validate options according to provided definitions.
69             sub validate {
70 211     211 0 309 my $optDefs= shift;
71              
72             # Get label and option flags, if present.
73 211 100       546 my $label= @_ & 1? shift : '';
74 211   66     911 ($label, my $flags)= split /:/, defined($label) && $label, 2;
75              
76             unshift @_,
77             label => defined($label) && $label,
78 211   100     1075 map { $_ => 1 } split //, defined($flags) && $flags;
  12   66     46  
79              
80             # Get key/value options.
81 211         290 my @options;
82              
83 211         349 foreach (_pairs \@_) {
84 809         1210 my $opt= lc $_->[0]; # option names are case insensitive
85 809         1206 local $_= $_->[1];
86              
87             # Convert an alias option name to its primary name.
88 809 100       1553 $opt= OPTION_ALIASES->{$opt} if exists OPTION_ALIASES->{$opt};
89              
90             # Confirm option name actually exists.
91 809 100       1812 exists $optDefs->{$opt} or croak qq(Unknown option '$opt');
92              
93             # Confirm and process option values and their types.
94 807         1232 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     4945 : ($type eq '*' or $optDefs->{$opt}[0] eq ref or croak qq(Option '$opt' may not be type '@{[ ref || '(SCALAR)' ]}'));
      33        
99              
100 807         1799 push @options, $opt => $_;
101             }
102              
103 209         1866 @options;
104             }
105              
106              
107             my %OPTIONS;
108              
109             sub import {
110 79     79   43625 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         249 my $bug= 'bug';
119              
120             # Caller may export bug() under a different name or suppress export.
121 77 100       167 if (exists $OPTIONS{bug}) {
122             # Don't export anything if explicity set to falsy.
123 4 100       19 $bug= $OPTIONS{bug} or return;
124              
125             # Export bug() under a different name.
126 2 100       189 $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         7  
  5         3268  
132 74         126 *{ (caller).'::'.$bug }= \&bug;
  74         414  
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 4294 my $self= bless { %OPTIONS, validate BUG_OPTIONS, @_ }, __PACKAGE__;
146              
147             # Get extra info to include with output.
148 53         143 my %info;
149              
150 53         233 @info{ (CALLER_INFO) }= caller;
151 53         132 $info{lineno}= "line $info{lineno}";
152              
153 53         259 $self->{info}= join(' ', map $info{$_}, grep $self->{$_}, CALLER_INFO);
154              
155             # Tie an array or scalar, for list or scalar context respectively.
156             # Implicit return (no return keyword) avoids "Bizarre copy of ARRAY in return".
157 53 100       105 if (wantarray) { $self->{data}= []; tie my @a, __PACKAGE__, $self; @a }
  7         9  
  7         22  
  7         28  
158 46         76 else { $self->{data}= \my $scalar; tie my $s, __PACKAGE__, $self; $s }
  46         131  
  46         206  
159             }
160              
161             # Just pass along self object constructed in bug(), which invokes these via tie().
162 46     46   88 sub TIESCALAR { $_[1] }
163 7     7   13 sub TIEARRAY { $_[1] }
164              
165             # Methods for tied array.
166 7     7   23 sub CLEAR { $_[0]->{data}= [] }
167 0     0   0 sub FETCHSIZE { @{ $_[0]->{data} } }
  0         0  
168              
169             # Shared methods: SCALAR ARRAY
170 66 100   66   104 sub FETCH { @_ == 1? ${ $_[0]->{data} } : $_[0]->{data}[ $_[1] ] }
  46         111  
171 66 100   66   150 sub STORE { @_ == 2? (${ $_[0]->{data} }= $_[1]) : ($_[0]->{data}[ $_[1] ]= $_[2]) }
  46         171  
172              
173             # Format and output captured values upon destruction of temporary tied variable.
174             sub DESTROY {
175 53     53   174 my $self= $_[0];
176 53         103 my $override= exists $self->{val};
177              
178             my ($data, $delims, $color, $multiline, $indices, $keyval, $ic, $lc, $vc)=
179 53         69 @{$self}{ qw(data delims color multiline indices keyval infocolor labelcolor valcolor) };
  53         174  
180              
181 53         78 my $isScalar= ref($data) eq 'SCALAR';
182              
183 53   100     168 $indices||= '';
184              
185             # Get terminal width if requested and available.
186 53 100 100     141 my $termW= (not $self->{noterm} and _isTerm($self->{out}))? _sttyWidth() || _tspWidth($self->{out}) : 0;
      100        
187              
188             # Get the pretty printer sub.
189 53         98 my $ppSub;
190              
191 53 100       90 if (defined $self->{pp}) {
192 7 100       7 eval {
193             # Caller specified sub in Module::sub form.
194 7         8 my $pp= $self->{pp};
195 7 100       191 my ($ppPN)= $pp=~/^(.+)::.+$/i or die qq(Invalid pretty-printer '$pp' specified: expected 'Module::sub' form);
196              
197 6         16 $ppPN=~s{::}{/}g; # get module path with slashes for require
198              
199             # Load the module.
200 6 100       7 eval { require "$ppPN.pm" } or die $@;
  6         899  
201              
202             # Confirm sub callable and save ref to it.
203 5     5   34 no strict 'refs';
  5         8  
  5         418  
204 5 100       5453 defined &$pp or die qq(Invalid pretty-printer '$pp');
205 4         11 $ppSub= \&$pp;
206             } or carp $@;
207             }
208              
209             # If no pretty printer specified or loading it didn't work, try the default.
210 5   66 5   36 $ppSub||= do { no warnings 'once'; require Data::Dumper; $Data::Dumper::Indent= 1; \&Data::Dumper::Dumper };
  5         7  
  5         3581  
  53         111  
  49         2055  
  49         21442  
  49         117  
211              
212             # Make a string representation of the data.
213             my $toString= sub {
214 61     61   75 my $color= $_[0]; # color the text with ANSI colors?
215 61 100 100     244 my $ml= $_[1] || $multiline || $indices? "\n" : ''; # multiline?
216              
217 61         145 my $label= $self->{label};
218 61         76 my $info= $self->{info};
219              
220 61         62 local $_;
221              
222             # Return a string representation of the value, coloring if needed.
223             my $cv= sub {
224 74 100       202 my $txt= ref $_[0]? $ppSub->($_[0]) : defined $_[0]? $_[0] : 'UNDEF';
    100          
225 74 100 66     860 ($color and $vc)? colored($txt, $vc) : $txt;
226 61         176 };
227              
228             # Make a string representation of the data.
229 61         73 my $i= 0;
230              
231             my $vals=
232             $ml.(
233             join $ml || ' ',
234 70 100       686 map { $ml? " $_" : $_ } # multiline?
235             $override? ( $cv->($self->{val}) ) # vals => ... override used
236             : $isScalar? ( $cv->($$data) ) # single scalar
237 4   66     23 : $keyval? ( map { ($indices && $i++.': ').$cv->($_->[0]).' => '.$cv->($_->[1]) } _pairs($data) ) # list of key/val pairs
238 61 100 100     243 : ( map { ($indices && $i++.': ').$cv->($_) } @$data ) # list
  12 100 66     22  
    100          
239             ).$ml;
240              
241             # Format info, label and vals, coloring if needed.
242 61 50 33     134 $info= ($color and $ic)? colored($info, $ic).': ' : "$info: " if length $info;
    100          
243 61 100 66     218 $label= ($color and $lc)? colored($label, $lc).'=' : "$label=" if length $label;
    50          
244 61 100 66     562 $vals= '('.$vals.')' if $delims or defined($delims) and ($ml or not $color or length($vals) == 0);
      66        
      66        
245              
246 61         314 $info.$label.$vals;
247 53         403 };
248              
249             # Call toString once for possible sizing, then again if necessary for coloring and/or wrapping.
250 53         65 my $str;
251              
252 53   100     134 $str= $toString->($color and not $termW);
253 53 100       100 $str= $toString->(defined($color), $termW < length $str) if $termW;
254              
255             # Ouput the string.
256 53         63 print { $self->{out} } $str."\n";
  53         1110  
257             }
258              
259              
260             # Catch if user code attempts to use this like a regular object.
261             sub AUTOLOAD {
262 0     0     our $AUTOLOAD;
263 0           croak qq(Attempt to call unneeded non-existent subroutine '$AUTOLOAD': class @{[ __PACKAGE__ ]} intended for inline logging only);
  0            
264             }
265              
266             # If these get called, they need to be no-ops, since we're only using the tied array for logging.
267       6     sub EXTEND { }
268       0     sub STORESIZE { }
269              
270              
271              
272             1;
273              
274             __END__