File Coverage

blib/lib/Runtime/Debugger.pm
Criterion Covered Total %
statement 29 89 32.5
branch 0 18 0.0
condition 0 7 0.0
subroutine 10 21 47.6
pod 3 3 100.0
total 42 138 30.4


line stmt bran cond sub pod time code
1             package Runtime::Debugger;
2              
3             =head1 LOGO
4              
5             ____ _ _
6             | _ \ _ _ _ __ | |_(_)_ __ ___ ___
7             | |_) | | | | '_ \| __| | '_ ` _ \ / _ \
8             | _ <| |_| | | | | |_| | | | | | | __/
9             |_| \_\\__,_|_| |_|\__|_|_| |_| |_|\___|
10              
11             ____ _
12             | _ \ ___| |__ _ _ __ _ __ _ ___ _ __
13             | | | |/ _ \ '_ \| | | |/ _` |/ _` |/ _ \ '__|
14             | |_| | __/ |_) | |_| | (_| | (_| | __/ |
15             |____/ \___|_.__/ \__,_|\__, |\__, |\___|_|
16             |___/ |___/
17              
18             =cut
19              
20 1     1   68085 use 5.012;
  1         5  
21 1     1   5 use strict;
  1         3  
  1         23  
22 1     1   6 use warnings;
  1         10  
  1         33  
23 1     1   636 use Data::Dumper;
  1         6930  
  1         64  
24 1     1   528 use Term::ReadLine;
  1         2640  
  1         35  
25 1     1   606 use Term::ANSIColor qw( colored );
  1         8383  
  1         749  
26 1     1   553 use PadWalker qw( peek_my peek_our );
  1         670  
  1         67  
27 1     1   7 use feature qw( say );
  1         2  
  1         134  
28 1     1   487 use parent qw( Exporter );
  1         297  
  1         6  
29 1     1   602 use subs qw( p uniq );
  1         23  
  1         5  
30              
31             our @EXPORT = qw(
32             run
33             h
34             p
35             hist
36             uniq
37             );
38              
39             =head1 NAME
40              
41             Runtime::Debugger - Easy to use REPL with existing lexicals support.
42              
43             =head1 VERSION
44              
45             Version 0.04
46              
47             =cut
48              
49             our $VERSION = '0.04';
50              
51              
52             =head1 SYNOPSIS
53              
54             tl;dr - Easy to use REPL with existing lexicals support.
55              
56             (empahsis on "existing" since I have not yet found this support
57             in others modules).
58              
59             Try with this command line:
60              
61             perl -MRuntime::Debugger -E 'my $str1 = "str-1"; our $str2 = "str-2"; my @arr1 = "arr-1"; our @arr2 = "arr-2"; my %hash1 = qw(hash 1); our %hash2 = qw(hash 2); eval run; say $@'
62              
63             =head1 DESCRIPTION
64              
65             One can usually just do this:
66              
67             # Insert this where you want to pause:
68             $DB::single = 1;
69              
70             # Then run the perl debugger to navigate there quickly:
71             PERLDBOPT='Nonstop' perl -d my_script
72              
73             If that works for then great and dont' bother using this module!
74              
75             Unfortunately for me, it was not working due to the scenario
76             in which a script evals another perl test file and I would have
77             liked to pause inside the test and see whats going on without
78             having to keep rerunning the whole test over and over.
79              
80             This module basically drops in a read,evaludate,print loop (REPL)
81             whereever you need like so:
82              
83             use Runtime::Debugger;
84             eval run; # Not sure how to avoid using eval here while
85             # also being able to keep the lexical scope.
86             # Any ideas ? :)
87              
88             Press tab to autocomplete any lexical variables in scope (where "eval run" is found).
89              
90             Saves history locally.
91              
92             Can use 'p' to pretty print a variable or structure.
93              
94             =head2 New Variables
95              
96             Currently its not possible to create any new lexicals variables
97             while I have not yet found a way to run "eval" with a higher scope of lexicals.
98             (perhaps there is another way?)
99              
100             You can make global variables though if:
101              
102             - By default ($var=123)
103             - Using our (our $var=123)
104             - Given the full path ($My::var = 123)
105              
106             =head1 SUBROUTINES/METHODS
107              
108             =cut
109              
110             #
111             # API
112             #
113              
114             =head2 run
115              
116             Runs the REPL (dont forget eval!)
117              
118             eval run
119              
120             Sets C<$@> to the exit reason like 'INT' (Control-C) or 'q' (Normal exit/quit).
121              
122             =cut
123              
124             sub run {
125 0     0 1   <<'CODE';
126             my $repl = Runtime::Debugger->_init;
127             while ( 1 ) {
128             eval $repl->_step;
129             $repl->_show_error($@) if $@;
130             }
131             CODE
132             }
133              
134             =head2 h
135              
136             Show help section.
137              
138             =cut
139              
140             sub h {
141 0     0 1   say colored( <<"HELP", "YELLOW" );
142              
143             h - Show this help section.
144             q - Quit debugger.
145             TAB - Show available lexical variables.
146             p DATA [#N] - Prety print data (with optional depth),
147             hist [N=20] - Show last N commands.
148              
149             HELP
150             }
151              
152             =head2 p
153              
154             Data::Dumper::Dump anything.
155              
156             p 123
157             p [1, 2, 3]
158              
159             Can adjust the maxdepth (default is 1) to see with: "#Number".
160              
161             p { a => [1, 2, 3] } #1
162              
163             Output:
164              
165             {
166             'a' => 'ARRAY(0x55fd914a3d80)'
167             }
168              
169             Set maxdepth to '0' to show all nested structures.
170              
171             =cut
172              
173             sub p {
174              
175             # Use same function to change maxdepth of whats shown.
176 0     0     my $maxdepth =
177             1; # Good default to often having to change it during display.
178 0 0 0       if ( @_ > 1 and $_[-1] =~ / ^ --maxdepth=(\d+) $ /x )
179             { # Like with "tree" command.
180 0           $maxdepth = $1;
181 0           pop @_;
182             }
183              
184 0           my $d = Data::Dumper
185             ->new( \@_ )
186             ->Sortkeys( 1 )
187             ->Terse( 1 )
188             ->Indent( 1 )
189             ->Maxdepth( $maxdepth );
190              
191 0 0         return $d->Dump if wantarray;
192 0           print $d->Dump;
193             }
194              
195             =head2 hist
196              
197             Show history of commands.
198              
199             By default will show 20 commands:
200              
201             hist
202              
203             Same thing:
204              
205             hist 20
206              
207             Can show more:
208              
209             hist 50
210              
211             =cut
212              
213             sub hist {
214 0     0 1   my ( $self, $levels ) = @_;
215 0   0       $levels //= 20;
216 0           my @history = $self->_history;
217              
218 0 0         if ( $levels < @history ) {
219 0           @history = splice @history, -$levels;
220             }
221              
222 0           for my $index ( 0 .. $#history ) {
223 0           printf "%s %s\n",
224             colored( $index + 1, "YELLOW" ),
225             colored( $history[$index], "GREEN" );
226             }
227             }
228              
229             =head2 uniq
230              
231             Return a list of uniq values.
232              
233             =cut
234              
235             sub uniq (@) {
236 0     0     my %h;
237 0           grep { not $h{$_}++ } @_;
  0            
238             }
239              
240             #
241             # Internal
242             #
243              
244             sub _init {
245 0     0     my ( $class ) = @_;
246 0           my $self = bless {
247             history_file => "$ENV{HOME}/.runtime_debugger.info",
248             term => Term::ReadLine->new( $class ),
249             }, $class;
250 0           my $attribs = $self->{attribs} = $self->{term}->Attribs;
251              
252 0           $self->{term}->ornaments( 0 ); # Remove underline from terminal.
253              
254             # Restore last history.
255 0 0         if ( -e $self->{history_file} ) {
256 0           my @history;
257 0 0         open my $fh, '<', $self->{history_file} or die $!;
258 0           while ( <$fh> ) {
259 0           chomp;
260 0           push @history, $_;
261             }
262 0           close $fh;
263 0           $self->_history( @history );
264             }
265              
266             # https://metacpan.org/pod/Term::ReadLine::Gnu#Custom-Completion
267             # Definition for list_completion_function is here: Term/ReadLine/Gnu/XS.pm
268             $attribs->{completion_entry_function} =
269 0           $attribs->{list_completion_function};
270              
271             # Remove these as break chars so that we can complete:
272             # "$scalar", "@array", "%hash"
273             # ("%" was already not in the list).
274 0           $attribs->{completer_word_break_characters} =~ s/ [\$@] //xg;
275              
276             # Setup some signal hnndling.
277 0           for my $signal ( qw( INT TERM HUP ) ) {
278 0     0     $SIG{$signal} = sub { $self->_exit( $signal ) };
  0            
279             }
280              
281 0           $self;
282             }
283              
284             sub _exit {
285 0     0     my ( $self, $how ) = @_;
286              
287             # Save current history.
288 0 0         open my $fh, '>', $self->{history_file} or die $!;
289 0           say $fh $_ for $self->_history;
290 0           close $fh;
291              
292             # This will reset the terminal similar to
293             # what these should do:
294             # - "reset"
295             # - "tset"
296             # - "stty echo"
297 0           $self->{term}->deprep_terminal;
298              
299 0           die "Exit via '$how'\n";
300             }
301              
302             sub _history {
303 0     0     my $self = shift;
304              
305             # Setter.
306 0 0         return $self->{term}->SetHistory( @_ ) if @_;
307              
308             # Getter.
309             # Last command should be the first you see upon hiting arrow up
310             # and also without any duplicates.
311 0           reverse uniq reverse $self->{term}->GetHistory;
312             }
313              
314             sub _step {
315 0     0     my ( $self ) = @_;
316              
317             # Current lexical variables in scope.
318             # Note: this block could be moved to _init, but the intent
319             # was to be able to see newly added lexcicals
320             # (which does not seem to be possible).
321             #
322             # But global variable can be created and therefore it is
323             # best to keep this block here to run per command.
324 0           my $lexicals = peek_my( 1 );
325 0           my $globals = peek_our( 1 );
326 0           my @words = sort keys %$lexicals, keys %$globals;
327 0           $self->{attribs}->{completion_word} = \@words;
328              
329 0   0       my $input = $self->{term}->readline( "perl>" ) // '';
330              
331             # Change '#1' to '--maxdepth=1'
332 0 0         if ( $input =~ / ^ p\b /x ) {
333 0           $input =~ s/
334             \s*
335             \#(\d) #2 to --maxdepth=2
336             \s*
337             $ /, '--maxdepth=$1'/x;
338             }
339              
340             # Change "COMMAND ARG" to "$repl->COMMAND(ARG)".
341 0           $input =~ s/ ^
342             (
343             hist
344             ) \b
345             (.*)
346             $ /\$repl->$1($2)/x;
347              
348 0 0         $self->_exit( $input ) if $input eq 'q';
349              
350 0           $input;
351             }
352              
353             sub _show_error {
354 0     0     my ( $self, $error ) = @_;
355              
356             # Remove eval line numbers.
357 0           $error =~ s/ at \(eval .+//;
358              
359 0           say colored( $error, "RED" );
360             }
361              
362             =head1 ENVIRONMENT
363              
364             Install required library:
365              
366             sudo apt install libreadline-dev
367              
368             =head1 SEE ALSO
369              
370             =head2 L
371              
372             Great extendable module!
373              
374             Unfortunately, I did not find a way to get the lexical variables
375             in a scope. (maybe missed a plugin?!)
376              
377             =head2 L
378              
379             This module also looked nice, but same issue.
380              
381             =head1 AUTHOR
382              
383             Tim Potapov, C<< >>
384              
385             =head1 BUGS
386              
387             - L
388              
389             Please report any (other) bugs or feature requests to L.
390              
391              
392             =head1 SUPPORT
393              
394             You can find documentation for this module with the perldoc command.
395              
396             perldoc Runtime::Debugger
397              
398              
399             You can also look for information at:
400              
401             L
402             L
403              
404              
405             =head1 LICENSE AND COPYRIGHT
406              
407             This software is Copyright (c) 2022 by Tim Potapov.
408              
409             This is free software, licensed under:
410              
411             The Artistic License 2.0 (GPL Compatible)
412              
413              
414             =cut
415              
416             1; # End of Runtime::Debugger