File Coverage

blib/lib/Term/ReadLine/Repl.pm
Criterion Covered Total %
statement 81 156 51.9
branch 38 86 44.1
condition 11 28 39.2
subroutine 9 14 64.2
pod 3 3 100.0
total 142 287 49.4


line stmt bran cond sub pod time code
1 1     1   153098 use strict;
  1         3  
  1         44  
2 1     1   6 use warnings;
  1         3  
  1         184  
3              
4             package Term::ReadLine::Repl;
5              
6             our $VERSION = '0.0.1';
7              
8             =head1 NAME
9              
10             Term::ReadLine::Repl - A batteries included interactive Term::ReadLine REPL module
11              
12             =head1 SYNOPSIS
13              
14             use Term::ReadLine::Repl;
15              
16             # A simple repl
17             my $repl = Term::ReadLine::Repl->new(
18             {
19             name => 'myrepl',
20             cmd_schema => {
21             ls => {
22             exec => sub { my @list = qw(a b c); print for @list },
23             },
24             },
25             }
26             );
27              
28             # A complete repl
29             $repl = Term::ReadLine::Repl->new(
30             {
31             name => 'myrepl',
32             prompt => '(%s)>',
33             cmd_schema => {
34             stats => {
35             exec => \&get_stats,
36             args => [
37             {
38             refresh => undef,
39             host => 'hostname',
40             guest => 'guestname',
41             list => 'host|guest',
42             cluster => undef,
43             }
44             ],
45             },
46             },
47             passthrough => 1,
48             hist_file => '/path/to/.hist_file',
49             get_opts => \&arg_parse,
50             custom_logic => \&my_custom_loop_ctrl,
51             }
52             );
53              
54             $repl->run();
55              
56             =head1 DESCRIPTION
57              
58             C provides a simple framework for building interactive
59             command-line REPLs (Read-Eval-Print Loops) on top of L. It
60             handles tab completion, command history, a built-in help system, and optional
61             passthrough to shell commands, so you can focus on defining your commands
62             rather than plumbing the terminal interaction.
63              
64             =head2 Overview
65              
66             You define your commands and their arguments via the C hashref
67             passed to C. Each command maps to an C coderef that is called
68             when the user types that command, and an optional C structure that drives
69             tab completion. Once constructed, calling C drops the user into an
70             interactive prompt.
71              
72             The module handles the following automatically:
73              
74             =over 4
75              
76             =item *
77              
78             B - command names and their arguments are completed from the
79             C definition. Passthrough commands (prefixed with C) are
80             excluded.
81              
82             =item *
83              
84             B - input history is maintained in-session via
85             L, and can be persisted across sessions by supplying a
86             C path.
87              
88             =item *
89              
90             B - C and C/C are injected automatically
91             into every REPL.
92              
93             =item *
94              
95             B - when C is enabled, any input prefixed with
96             C is forwarded directly to the system shell, making it easy to run one-off
97             shell commands without leaving the REPL.
98              
99             =item *
100              
101             B - the C and C callbacks let you
102             plug L parsing and arbitrary mid-loop logic into the REPL without
103             having to subclass or modify the module.
104              
105             =back
106              
107             =head1 CONSTRUCTOR
108              
109             =over 4
110              
111             =item C
112              
113             Creates and returns a new C object. Accepts a hashref
114             with the following keys:
115              
116             =over 4
117              
118             =item C (required)
119              
120             A string used as the name of the REPL, displayed in the welcome message and
121             optionally interpolated into the prompt via C<%s>.
122              
123             =item C (required)
124              
125             A hashref defining the available commands. Each key is a command name, and
126             its value is a hashref with the following keys:
127              
128             =over 4
129              
130             =item C (required)
131              
132             A coderef that is called when the command is invoked. Any arguments supplied
133             on the command line (after the command name) are passed to the coderef.
134              
135             =item C (optional)
136              
137             An arrayref of hashrefs describing the command's arguments for tab completion.
138             Each hashref maps an argument name to either C (flag, no value expected)
139             or a string describing the expected value (used as a completion hint).
140              
141             =back
142              
143             =item C (optional)
144              
145             A C-style format string for the prompt. C<%s> is replaced with the
146             REPL name. Defaults to C<(repl)>>.
147              
148             =item C (optional)
149              
150             When set to a true value, any input beginning with C is passed directly to
151             the system shell. For example, C would run C. Defaults to C<0>.
152              
153             =item C (optional)
154              
155             Path to a file used for persistent command history. History is loaded on
156             startup and saved on exit. If not specified, history is not persisted.
157              
158             =item C (optional)
159              
160             A coderef to a L parsing function. When provided, it is called
161             before each command dispatch with C<@ARGV> populated from the current input line.
162              
163             =item C (optional)
164              
165             A coderef invoked on each loop iteration before command dispatch. Receives an
166             arrayref of the parsed input tokens. May return a hashref with the following
167             optional keys:
168              
169             =over 4
170              
171             =item C
172              
173             Set to C<'next'> to skip to the next loop iteration, or C<'last'> to exit
174             the REPL loop.
175              
176             =item C
177              
178             A replacement C hashref to swap in for subsequent iterations.
179              
180             =back
181              
182             =back
183              
184             =back
185              
186             =head1 METHODS
187              
188             =over 4
189              
190             =item C
191              
192             Launches the interactive REPL session. Prints a welcome message, then enters
193             the read-eval-print loop until the user types C, C, or C.
194             Saves history on exit if C was configured.
195              
196             =item C
197              
198             Validates the constructor argument hashref. Croaks with a descriptive message
199             if any required arguments are missing or if any values have an unexpected type.
200             Called automatically by C.
201              
202             =back
203              
204             =head1 BUILT-IN COMMANDS
205              
206             The following commands are automatically added to every REPL:
207              
208             =over 4
209              
210             =item C
211              
212             Prints all available commands and their arguments.
213              
214             =item C / C
215              
216             Exits the REPL session.
217              
218             =back
219              
220             =head1 TAB COMPLETION
221              
222             Tab completion is provided automatically for command names and their defined
223             arguments. Completions are driven by the C key in each command's schema.
224             Passthrough commands (those beginning with C) are excluded from completion.
225              
226             =head1 AUTHORS
227              
228             Written by John R. Copyright (c) 2026
229              
230             =head1 LICENSE
231              
232             This library is free software; you can redistribute it and/or modify it under
233             the same terms as Perl itself.
234              
235             See L for more information.
236              
237             =cut
238              
239 1     1   776 use Data::Dumper;
  1         11871  
  1         106  
240 1     1   1201 use Term::ANSIColor;
  1         14887  
  1         105  
241 1     1   995 use Term::ReadLine;
  1         4169  
  1         56  
242 1     1   8 use Carp qw(croak);
  1         2  
  1         2893  
243              
244             sub new {
245 16     16 1 308246 my ($class, $args) = @_;
246              
247 16         67 $class->validate_args($args);
248              
249             my $self = {
250             name => $args->{name} // 'repl',
251             prompt => defined $args->{prompt} ? sprintf $args->{prompt}, $args->{name} : '(repl)>',
252             cmd_schema => $args->{cmd_schema},
253             passthrough => $args->{passthrough} // 0,
254             hist_file => $args->{hist_file},
255             get_opts => $args->{get_opts},
256             custom_logic => $args->{custom_logic},
257 7 100 50     129 };
      50        
258              
259             # Add builtin commands.
260 7         22 $self->{cmd_schema}{help}={};
261 7         25 $self->{cmd_schema}{quit}={};
262              
263 7         18 bless $self, $class;
264              
265 7         54 return $self;
266             }
267              
268             sub validate_args {
269 16     16 1 43 my ($self, $args) = @_;
270              
271             # Ensure name and cmd_schema exist (required args)
272 16 100 66     127 croak "name is a required arg!" unless exists $args->{name} && defined $args->{name};
273 15 100 66     85 croak "cmd_schema is a required arg!" unless exists $args->{cmd_schema} && defined $args->{cmd_schema};
274              
275             # Ensure cmd_schema is a hashref
276 14 100       58 croak "cmd_schema is NOT a hashref!" unless ref $args->{cmd_schema} eq 'HASH';
277              
278             # Ensure each cmd has an exec key and is a coderef
279 13         24 for my $cmd (keys %{$args->{cmd_schema}}) {
  13         46  
280 14         33 my $schema = $args->{cmd_schema}{$cmd};
281              
282 14 100       57 croak "'$cmd' missing exec key!" unless defined $schema->{exec};
283              
284 13 100       57 croak "'$cmd' exec is NOT a coderef!" unless ref $schema->{exec} eq 'CODE';
285              
286             # Ensure that args is an array
287 12 100 66     49 if (exists $schema->{args} && defined $schema->{args}) {
288 4 100       57 croak "'$cmd' args is NOT a arrayref!" unless ref $schema->{args};
289              
290 3 100       5 croak "'$cmd' args array is empty!" if scalar @{$schema->{args}} < 1;
  3         45  
291              
292 2         4 for my $arg (@{$schema->{args}}) {
  2         8  
293 2 100       25 croak "'$cmd' non-hashref found in args arrayref!" unless ref $arg eq 'HASH';
294             }
295             }
296             }
297              
298             # Ensure get_ops is a coderef if present
299 8 100 66     39 if (exists $args->{get_opts} && defined $args->{get_opts}) {
300 2 100       21 croak "get_opts is NOT a coderef!" unless ref $args->{get_opts} eq 'CODE';
301             }
302             }
303              
304             sub run {
305 0     0 1 0 my ($self) = @_;
306              
307 0         0 my $term = Term::ReadLine->new('Simple Shell');
308 0         0 my $attribs = $term->Attribs;
309              
310 0 0       0 $self->_read_history($term) if defined $self->{hist_file};
311              
312 0         0 print colored(sprintf("Welcome to $self->{name} shell!"), 'green underline italic bold'), "\n";
313 0         0 print colored(sprintf("Type 'help' for more options, to auto complete."), 'green bold'), "\n";
314              
315             # Tab completion.
316 0     0   0 $attribs->{completion_function} = sub { return $self->_tab_complete(@_) };
  0         0  
317 0         0 my $prompt = colored(sprintf("$self->{prompt} "), 'green');
318              
319 0         0 $|++;
320              
321             # Simple REPL loop.
322 0         0 while (defined (my $input = $term->readline($prompt))) {
323 0         0 chomp $input;
324 0 0       0 last if ($input =~ /^(exit|quit)$/);
325              
326 0 0       0 next unless $input;
327              
328 0 0       0 if ($input =~ 'help') {
329 0         0 $self->_help();
330 0         0 next;
331             }
332              
333 0         0 my @args = split(/\s+/, $input);
334              
335             # Command line passthrough.
336 0 0 0     0 if ($self->{passthrough} && @args && $args[0] =~ /^\!/) {
      0        
337 0         0 $args[0] =~ s/\!//g;
338 0         0 system(@args);
339 0         0 next;
340             }
341              
342 0 0       0 if (defined $self->{get_opts}) {
343             # Clobber ARGV for getopts parsing, doesn't matter because client
344             # code parser will slurp args outta @ARGV again right away.
345 0         0 @ARGV = @args;
346 0         0 $self->{get_opts}->();
347             }
348              
349             # Custom loop logic.
350             # User custom function can return a hashref like the following.
351             # {
352             # action => next|last|undef,
353             # schema => $schema, # Where schema is a hashref containing any changes your custom logic might make to cmd_schema.
354             # }
355 0 0       0 if (defined $self->{custom_logic}) {
356 0         0 my $result = eval {
357 0         0 $self->{custom_logic}->(\@args);
358             };
359              
360 0 0 0     0 if (defined $result && ref $result eq 'HASH') {
361 0 0       0 if (defined $result->{action}) {
362 0 0       0 next if $result->{action} eq 'next';
363 0 0       0 last if $result->{action} eq 'last';
364             }
365 0 0       0 if (defined $result->{schema}) {
366 0         0 $self->{cmd_schema} = $result->{schema};
367             }
368             }
369             }
370              
371 0         0 my $cmd = shift @args;
372              
373 0 0       0 if (exists $self->{cmd_schema}{$cmd}) {
374 0         0 $self->{cmd_schema}{$cmd}{exec}->(@args);
375             } else {
376 0         0 print "No such command '$cmd' run 'help' to see options\n";
377             }
378             }
379 0         0 print "\n" . colored(sprintf("Goodbye!"), 'green bold underline italic'), "\n";
380 0 0       0 $self->_save_history($term) if defined $self->{hist_file};
381             }
382              
383             sub _tab_complete {
384 5     5   20150 my ($self, $text, $line) = @_;
385              
386             # Don't auto complete on passthroughs.
387 5 100       31 return () if $line =~ /^\!/;
388              
389             # Split the current line into words.
390 4         28 my @words = split(/\s+/, $line);
391 4         12 my @complete_words = @words;
392 4 100       24 pop @complete_words unless $line =~ /\s$/;
393              
394 4 50       16 if (@words >= 1) {
395 4         8 my $cmd = $words[0];
396 4         10 my $arg_index = (scalar(@complete_words) - 1); # -1 because first word is always $cmd
397              
398 4 100       17 if ($self->{cmd_schema}{$cmd}) {
399 2         6 my $schema = $self->{cmd_schema}{$cmd};
400              
401             # None of the below make sense unless we have args.
402 2 50       10 return () unless $schema->{args};
403              
404 2         4 my $opt_arg_index = $arg_index -1;
405              
406             # If next word matches args key, go into optargs
407 2 50 33     16 if (scalar @complete_words && exists $schema->{args}[$opt_arg_index]{$complete_words[-1]}) {
408 0         0 my $opt_arg = $schema->{args}[$opt_arg_index]{$complete_words[-1]};
409 0 0       0 return "<$opt_arg>" if defined $opt_arg;
410             }
411              
412             # Count number of opt args in command to subtract from $arg_index.
413 2         5 my $num_opt_args=0;
414 2         5 my @all_opt_args;
415 2         3 for my $arg (@{$schema->{args}}) {
  2         7  
416 2         4 for my $key (keys %{$arg}) {
  2         7  
417 6         13 my $value = $arg->{$key};
418 6 100       23 push @all_opt_args, $key if defined $value;
419             }
420             }
421 2         5 for my $word (@complete_words) {
422 2         6 for my $opt_arg (@all_opt_args) {
423 4 50       12 $num_opt_args++ if ($word eq $opt_arg);
424             }
425             }
426 2         5 $arg_index = $arg_index - $num_opt_args;
427              
428 2         4 my $args = @{$schema->{args}}[$arg_index];
  2         7  
429 2         3 my @keys = keys %{$args};
  2         7  
430 2 50       6 return () unless @keys;
431 2         6 return grep { /^\Q$text/ } @keys;
  6         72  
432             }
433             }
434              
435             # If we're completing the first word
436 2 50       8 if (@words <= 1) {
437 2         5 my $cmd = $words[0];
438 2         3 my @cmds = keys %{$self->{cmd_schema}};
  2         9  
439 2         7 return grep { /^\Q$text/ } @cmds;
  8         127  
440             }
441              
442             # No completion For anything beyond second word.
443 0           return ();
444             }
445              
446              
447             sub _help {
448 0     0     my ($self) = @_;
449              
450 0           my $output;
451 0           for my $cmd (keys %{$self->{cmd_schema}}) {
  0            
452 0           $output .= "$cmd\n";
453 0 0         next unless $self->{cmd_schema}{$cmd}{args};
454 0           for my $args (sort @{$self->{cmd_schema}{$cmd}{args}} ) {
  0            
455 0           $output .= " ";
456 0           for my $arg (keys %{$args}) {
  0            
457 0           my $opt = $args->{$arg};
458 0           $output .= "$arg";
459 0 0         $output .= defined $opt ? "=<$opt>, " : ", ";
460             }
461 0           substr($output, -1) = ""; # Remove trailing space
462 0           substr($output, -1) = ""; # Remove trailing ,
463 0           $output .= "\n";
464             }
465             }
466 0           print "$output";
467             }
468              
469             sub _read_history {
470 0     0     my ($self, $term) = @_;
471              
472 0 0         if (-f $self->{hist_file}) {
473 0 0         open my $fh, '<', $self->{hist_file} or warn "Couldn't read auto bal history file: $!";
474 0           while (my $line = <$fh>) {
475 0           chomp $line;
476 0           $term->addhistory($line);
477             }
478 0           close $fh;
479             }
480             }
481              
482             sub _save_history {
483 0     0     my ($self, $term) = @_;
484              
485 0           my $attribs = $term->Attribs;
486              
487 0 0         open my $fh, '>>', $self->{hist_file} or warn "Couldn't save auto bal history: $!";
488 0 0         if ($term->ReadLine =~ /Gnu/) {
489 0           for my $line ($term->GetHistory) {
490 0           print $fh "$line\n";
491             }
492             }
493 0           close $fh;
494             }
495              
496             1;