File Coverage

blib/lib/Term/ReadLine/Repl.pm
Criterion Covered Total %
statement 81 157 51.5
branch 38 86 44.1
condition 11 28 39.2
subroutine 9 14 64.2
pod 3 3 100.0
total 142 288 49.3


line stmt bran cond sub pod time code
1 1     1   129319 use strict;
  1         2  
  1         38  
2 1     1   6 use warnings;
  1         2  
  1         147  
3              
4             package Term::ReadLine::Repl;
5              
6             our $VERSION = '0.0.2';
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 L. Radford, 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   715 use Data::Dumper;
  1         11118  
  1         91  
240 1     1   890 use Term::ANSIColor;
  1         13915  
  1         103  
241 1     1   940 use Term::ReadLine;
  1         3906  
  1         46  
242 1     1   8 use Carp qw(croak);
  1         2  
  1         2825  
243              
244             sub new {
245 18     18 1 306995 my ($class, $args) = @_;
246              
247 18         66 $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     88 };
      50        
258              
259             # Add builtin commands.
260 7         20 $self->{cmd_schema}{help}={};
261 7         16 $self->{cmd_schema}{quit}={};
262              
263 7         24 bless $self, $class;
264              
265 7         48 return $self;
266             }
267              
268             sub validate_args {
269 18     18 1 40 my ($self, $args) = @_;
270              
271             # Ensure name and cmd_schema exist (required args)
272 18 100 66     134 croak "name is a required arg!" unless exists $args->{name} && defined $args->{name};
273 17 100 66     83 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 16 100       66 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 15         25 for my $cmd (keys %{$args->{cmd_schema}}) {
  15         52  
280 16         34 my $schema = $args->{cmd_schema}{$cmd};
281              
282 16 100       62 croak "'$cmd' missing exec key!" unless defined $schema->{exec};
283              
284 15 100       54 croak "'$cmd' exec is NOT a coderef!" unless ref $schema->{exec} eq 'CODE';
285              
286             # Ensure that args is an array
287 14 100 66     60 if (exists $schema->{args} && defined $schema->{args}) {
288 6 100       60 croak "'$cmd' args is NOT a arrayref!" unless ref $schema->{args} eq 'ARRAY';
289              
290 3 100       7 croak "'$cmd' args array is empty!" if scalar @{$schema->{args}} < 1;
  3         27  
291              
292 2         5 for my $arg (@{$schema->{args}}) {
  2         8  
293 2 100       24 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     36 if (exists $args->{get_opts} && defined $args->{get_opts}) {
300 2 100       20 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 $input =~ s/^\s+|\s+$//g;
325 0 0       0 last if ($input =~ /^(exit|quit)$/);
326              
327 0 0       0 next unless $input;
328              
329 0 0       0 if ($input eq 'help') {
330 0         0 $self->_help();
331 0         0 next;
332             }
333              
334 0         0 my @args = split(/\s+/, $input);
335              
336             # Command line passthrough.
337 0 0 0     0 if ($self->{passthrough} && @args && $args[0] =~ /^\!/) {
      0        
338 0         0 $args[0] =~ s/^\!//;
339 0         0 system(@args);
340 0         0 next;
341             }
342              
343 0 0       0 if (defined $self->{get_opts}) {
344             # Clobber ARGV for getopts parsing, doesn't matter because client
345             # code parser will slurp args outta @ARGV again right away.
346 0         0 @ARGV = @args;
347 0         0 $self->{get_opts}->();
348             }
349              
350             # Custom loop logic.
351             # User custom function can return a hashref like the following.
352             # {
353             # action => next|last|undef,
354             # schema => $schema, # Where schema is a hashref containing any changes your custom logic might make to cmd_schema.
355             # }
356 0 0       0 if (defined $self->{custom_logic}) {
357 0         0 my $result = eval {
358 0         0 $self->{custom_logic}->(\@args);
359             };
360              
361 0 0 0     0 if (defined $result && ref $result eq 'HASH') {
362 0 0       0 if (defined $result->{action}) {
363 0 0       0 next if $result->{action} eq 'next';
364 0 0       0 last if $result->{action} eq 'last';
365             }
366 0 0       0 if (defined $result->{schema}) {
367 0         0 $self->{cmd_schema} = $result->{schema};
368             }
369             }
370             }
371              
372 0         0 my $cmd = shift @args;
373              
374 0 0       0 if (exists $self->{cmd_schema}{$cmd}) {
375 0         0 $self->{cmd_schema}{$cmd}{exec}->(@args);
376             } else {
377 0         0 print "No such command '$cmd' run 'help' to see options\n";
378             }
379             }
380 0         0 print "\n" . colored(sprintf("Goodbye!"), 'green bold underline italic'), "\n";
381 0 0       0 $self->_save_history($term) if defined $self->{hist_file};
382             }
383              
384             sub _tab_complete {
385 5     5   19438 my ($self, $text, $line) = @_;
386              
387             # Don't auto complete on passthroughs.
388 5 100       27 return () if $line =~ /^\!/;
389              
390             # Split the current line into words.
391 4         26 my @words = split(/\s+/, $line);
392 4         12 my @complete_words = @words;
393 4 100       21 pop @complete_words unless $line =~ /\s$/;
394              
395 4 50       15 if (@words >= 1) {
396 4         8 my $cmd = $words[0];
397 4         9 my $arg_index = (scalar(@complete_words) - 1); # -1 because first word is always $cmd
398              
399 4 100       15 if ($self->{cmd_schema}{$cmd}) {
400 2         5 my $schema = $self->{cmd_schema}{$cmd};
401              
402             # None of the below make sense unless we have args.
403 2 50       9 return () unless $schema->{args};
404              
405 2         4 my $opt_arg_index = $arg_index -1;
406              
407             # If next word matches args key, go into optargs
408 2 50 33     18 if (scalar @complete_words && exists $schema->{args}[$opt_arg_index]{$complete_words[-1]}) {
409 0         0 my $opt_arg = $schema->{args}[$opt_arg_index]{$complete_words[-1]};
410 0 0       0 return "<$opt_arg>" if defined $opt_arg;
411             }
412              
413             # Count number of opt args in command to subtract from $arg_index.
414 2         4 my $num_opt_args=0;
415 2         3 my @all_opt_args;
416 2         5 for my $arg (@{$schema->{args}}) {
  2         7  
417 2         3 for my $key (keys %{$arg}) {
  2         8  
418 6         12 my $value = $arg->{$key};
419 6 100       20 push @all_opt_args, $key if defined $value;
420             }
421             }
422 2         4 for my $word (@complete_words) {
423 2         5 for my $opt_arg (@all_opt_args) {
424 4 50       13 $num_opt_args++ if ($word eq $opt_arg);
425             }
426             }
427 2         5 $arg_index = $arg_index - $num_opt_args;
428              
429 2         3 my $args = @{$schema->{args}}[$arg_index];
  2         6  
430 2         4 my @keys = keys %{$args};
  2         6  
431 2 50       6 return () unless @keys;
432 2         6 return grep { /^\Q$text/ } @keys;
  6         68  
433             }
434             }
435              
436             # If we're completing the first word
437 2 50       7 if (@words <= 1) {
438 2         3 my $cmd = $words[0];
439 2         4 my @cmds = keys %{$self->{cmd_schema}};
  2         8  
440 2         6 return grep { /^\Q$text/ } @cmds;
  8         68  
441             }
442              
443             # No completion For anything beyond second word.
444 0           return ();
445             }
446              
447              
448             sub _help {
449 0     0     my ($self) = @_;
450              
451 0           my $output;
452 0           for my $cmd (keys %{$self->{cmd_schema}}) {
  0            
453 0           $output .= "$cmd\n";
454 0 0         next unless $self->{cmd_schema}{$cmd}{args};
455 0           for my $args (sort @{$self->{cmd_schema}{$cmd}{args}} ) {
  0            
456 0           $output .= " ";
457 0           for my $arg (keys %{$args}) {
  0            
458 0           my $opt = $args->{$arg};
459 0           $output .= "$arg";
460 0 0         $output .= defined $opt ? " <$opt>, " : ", ";
461             }
462 0           substr($output, -1) = ""; # Remove trailing space
463 0           substr($output, -1) = ""; # Remove trailing ,
464 0           $output .= "\n";
465             }
466             }
467 0           print "$output";
468             }
469              
470             sub _read_history {
471 0     0     my ($self, $term) = @_;
472              
473 0 0         if (-f $self->{hist_file}) {
474 0 0         open my $fh, '<', $self->{hist_file} or warn "Couldn't read history file: $!";
475 0           while (my $line = <$fh>) {
476 0           chomp $line;
477 0           $term->addhistory($line);
478             }
479 0           close $fh;
480             }
481             }
482              
483             sub _save_history {
484 0     0     my ($self, $term) = @_;
485              
486 0           my $attribs = $term->Attribs;
487              
488 0 0         open my $fh, '>', $self->{hist_file} or warn "Couldn't save history file: $!";
489 0 0         if ($term->ReadLine =~ /Gnu/) {
490 0           for my $line ($term->GetHistory) {
491 0           print $fh "$line\n";
492             }
493             }
494 0           close $fh;
495             }
496              
497             1;