File Coverage

blib/lib/Term/Shell/MultiCmd.pm
Criterion Covered Total %
statement 163 380 42.8
branch 53 264 20.0
condition 29 132 21.9
subroutine 27 53 50.9
pod 10 10 100.0
total 282 839 33.6


line stmt bran cond sub pod time code
1              
2             package Term::Shell::MultiCmd;
3              
4 4     4   53234 use warnings;
  4         6  
  4         101  
5 4     4   12 use strict;
  4         4  
  4         65  
6 4     4   15 use Carp ;
  4         6  
  4         1194  
7              
8             =head1 NAME
9              
10             Term::Shell::MultiCmd - Nested Commands Tree in Shell Interface
11              
12             =cut
13              
14             our $VERSION = '3.01';
15              
16             =head1 SYNOPSIS
17              
18             # Examples are available with the distribution, under directory 'examples/'
19             # This one is named examples/synopsis.pl
20              
21             use Term::Shell::MultiCmd;
22             my @command_tree =
23             ( 'multi word command' =>
24             { help => "Help title.",
25             opts => 'force repeat=i',
26             exec => sub {
27             my ($o, %p) = @_ ;
28             print "$p{ARG0} was called with force=$p{force} and repeat=$p{repeat}\n"
29             },
30             },
31             'multi word another command' =>
32             { help => 'Another help title.
33             Help my have multi lines, the top one
34             would be used when one linear needed.',
35             comp => sub {
36             # this function would be called when use hits tab completion at arguments
37             my ($o, $word, $line, $start, $op, $opts) = @_ ;
38             # .. do something, then
39             return qw/a list of completion words/ ;
40             },
41             exec => sub { my ($o, %p) = @_ ; print "$p{ARG0} was called\n"},
42             },
43             'multi word third command' =>
44             { help => 'same idea',
45             comp => [qw/a list of words/], # this is also possible
46             exec => sub { my ($o, %p) = @_ ; print "$p{ARG0} was called. Isn't that fun?\n"},
47             },
48             'multi word' => 'You can add general help title to a path',
49             ) ;
50              
51             Term::Shell::MultiCmd
52             -> new()
53             -> populate( @command_tree )
54             -> loop ;
55              
56             print "All done, see you later\n" ;
57              
58             =head1 NOTE
59              
60             To get the most from a command line, it might be a good idea to get the latest versions of
61             Term::ReadLine and Term::ReadKey.
62             There are numberless ways of doing it, one of them is running 'cpan update Bundle::CPAN' (with a proper write permission).
63              
64             =cut
65             # some of my common utility functions:
66             sub _params($@) {
67              
68             # convert parameter to hash table, at this point,
69             # I wish perl would have followed python's function
70             # parameters scheme, or made Params::Smart standard.
71             # (Had anybody mentioned perl6?)
72              
73             # Note 1: this parameter processing takes time, and wouldn't
74             # be a good choise for frequently called functions.
75              
76             # Note 2: as parameters are suplied by developer, a bad
77             # would terminate the program - this is not a sandbox.
78              
79 34     34   28 my %ret ;
80 34         35 my $str = shift ;
81 34         94 for (split ' ', $str) {
82 231 50       558 /(\w+)([\=\:](.*))?/ or confess "_params can only take simple instructions
83             like key (must be provided), or key=value (value becomes default), or key= (default empty string)
84             " ;
85 231 100       530 $ret{$1} = $2 ? $3 : undef ;
86             }
87             # when called as OO, itemize self
88             # Note: this one wouldn't work with classes (as in Term::Shell::MultiCmd -> new )
89 34 50 66     132 $ret{self} = shift if $_[0] and ref $_[0] ;
90 34         61 while (@_) {
91 95         102 my ($k, $v) = (shift, shift) ;
92 95 50       237 $k =~ s/^\-?\-?// unless ref $k ;
93 95 50       133 croak "unknown parameter: '$k'\n expected params: $str\n" unless exists $ret{$k} ;
94 95         163 $ret{$k} = $v ;
95             } ;
96 34         81 while (my ($k, $v) = each %ret) {
97 231 50       493 croak "missing parameter: '$k'\n expected params: $str\n" unless defined $v ;
98             }
99             %ret
100 34         163 }
101              
102             sub _options {
103             # Parsing user's options, this function is more forgiving than _params
104 7     7   7 my $p = shift ;
105 7 50       20 my @p = ref $p ? @$p : split ' ', $p ;
106 7         7 my %p ; # now we have a complete set
107              
108             # use Getopt::Long 'GetOptionsFromArray' ; -- didn't work as I expected ..
109 4     4   2489 use Getopt::Long ;
  4         36925  
  4         18  
110 7         10 local @ARGV = @_ ;
111 7 50 33     15 if (@p and not eval { GetOptions( \%p, @p ) }) {
  0         0  
112 0   0     0 $p{_ERR_} = "$@ Expected " . join ', ', map {/(\w+)/ ; '-' . ($1 || $_)} sort @p ;
  0         0  
  0         0  
113 0         0 $p{_ERR_} .= "\n" ;
114             }
115 7   50     28 $p{ARGV} ||= [@ARGV] ; # all the leftover, in order
116 7         20 %p
117             }
118              
119             # we can't limit ourselves by 'use :5.10', not yet.
120 0     0   0 sub _say(@) { print join ('', @_) =~ /^\n*(.*?)\s*$/s, "\n" }
121              
122              
123             # module specific functions
124              
125             # Important Note:
126             # Do manipulate $o->{delimiter} and $o->{delimiterRE} ONLY if you know what you're doing ...
127              
128             sub _split($$) {
129 41     41   37 my ($o, $l) = @_ ;
130 4     4   2955 use Text::ParseWords 'quotewords';
  4         3624  
  4         2052  
131             # grep {defined $_ and $_ ne ''} quotewords $o->{delimiterRE} || '\s+', 0, $l
132 41 50 50     120 grep {defined and length } quotewords $o->{delimiterRE} || '\s+', 0, $l
  63         1953  
133             }
134              
135             sub _join($@) {
136 8     8   9 my $o = shift ;
137 8   50     26 join $o->{delimiter} || ' ', @_
138             }
139              
140             sub _travela($@) { # explicit array
141 8     8   11 my ($o) = shift ;
142 8   33     42 my ($c, $d, @w, @path) = ($o->{root} || $o->{cmds}, $o->{delimiter} || ' ', @_ );
      50        
143 8   66     43 while ( @w and 'HASH' eq ref $c ) {
144 15         16 my $w = shift @w ;
145 15 100       33 if (exists $c->{$w}) {
146 14         11 $c = $c->{$w} ;
147 14         13 push @path , $w ;# $path .= "$w ";
148 14         35 next ;
149             }
150 1         19 my @c = grep /^$w/, keys %$c ;
151 1 50       5 if(@c == 1) {
152 0         0 $c = $c->{$c[0]} ;
153 0         0 push @path, $c[0] ; # $path .= "$c[0] " ;
154 0         0 next ;
155             }
156 1 50       4 if (@c > 1 ) {
157 0         0 my $cmd = join $d, @path, $w ;
158 0         0 return "Ambiguous command: '$cmd'\n $w could mean: @c\n" ;
159             }
160              
161             # if @c == 0 : should I state the obvious? well, not with perl
162 1         2 unshift @w, $w ;
163 1         3 last ;
164             }
165 8         34 ($c, join ($d, @path), @w)
166             }
167              
168             sub _travel($$) {
169 8     8   17 my ($o, $c) = &_check_pager ; # clear $c pager sign, let cmd know about it.
170 8 50 33     59 ($o, $c) = &_check_sh_pipe if $o->{enable_sh_pipe} and not $o->{piper};
171 8         16 $c = _check_silent_aliases($o, $c);
172 8         17 _travela( $o, _split $o, $c )
173             }
174              
175             sub _expect_param_comp {
176 0     0   0 my($o, $word, $line, $pos, $op, $opt) = @_;
177             # This is ugly, Getopt::Long has many options, and
178             # caller can use any of them. However, my parsing would
179             # be limited.
180             # print "$opt\n" ;
181 0         0 my ($eq, $t) = $opt =~ /([\=\:])(\w)\W*$/ ;
182 0 0       0 my $type = ($t ?
    0          
    0          
    0          
    0          
183             $t eq 'i' ? 'Integer':
184             $t eq 'o' ? 'Extended Integer':
185             $t eq 's' ? 'String' :
186             $t eq 'f' ? 'Real Number' :
187             $t : $t ) ;
188 0 0       0 $type = "(optional) $type" if $eq eq ':' ;
189 0         0 ("$opt\nParameter Expected for -$op, type '$type'", $word)
190             }
191              
192             my $dlm = $; ; # cache this value, in case the developer changes it on the fly.
193             # Should I make it explicit '\034' value?
194              
195             sub _filter($@) {
196 0     0   0 my $w = shift ;
197 0         0 my $qr = qr/^\Q$w/ ;
198 0         0 grep /$qr/, sort grep {$_ ne $dlm}
199 0         0 'ARRAY' eq ref $_[0] ? @{$_[0]} :
200 0 0       0 'HASH' eq ref $_[0] ? (keys %{$_[0]}) :
  0 0       0  
201             @_ ;
202             }
203              
204             =head1 SUBROUTINES/METHODS
205              
206             =head2 new
207              
208             my $cli = new Term::Shell::MultiCmd ;
209             - or -
210             my $cli = Term::Shell::MultiCmd->new( [optional parameters ...] ) ;
211              
212             The parameters to the constructor are passed in hash form, preceding dash is optional.
213              
214             Optional Parameters for the new command:
215              
216             =over 4
217              
218             =item * -prompt
219              
220             my $cli = new Term::Shell::MultiCmd ( -prompt => 'myprompt') ;
221             - or -
222             my $cli = mew Term::Shell::MultiCmd ( -prompt => \&myprompt) ;
223              
224             Overwrite the default prompt 'shell'.
225             Rules are:
226              
227             If prompt is a CODE reference, call it in each loop cycle and display the results.
228             if it ends with a non-word character, display it as is.
229             Else, display it with the root-path (if exists) and '> ' characters.
230              
231             =item * -help_cmd
232              
233             Overwrite the default 'help' command, empty string would disable this command.
234              
235             =item * -quit_cmd
236              
237             Overwrite the default 'quit' command, empty string would disable this command.
238              
239             =item * -root_cmd
240              
241             my $cli = new Term::Shell::MultiCmd ( -root_cmd => 'root' ) ;
242              
243             This would enable the root command and set it to root.
244              
245             Unlike 'quit' and 'help', the 'root' command is a little unexpected. Therefore it is disabled by default. I
246             strongly recommend enabling this command when implementing a big, deep command tree. This allows the user rooting
247             in a node, then referring to this node thereafter. After enabling, use 'help root' (or whatever names you've chosen)
248             for usage manual.
249              
250             =item * -history_file
251              
252             my $cli = new Term::Shell::MultiCmd ( -history_file => "$ENV{HOME}/.my_progarms_data" ) ;
253              
254             This is the history file name. If present, try to load history from this file just
255             before the loop command, and try saving history in this file after the loop command.
256             Default is an empty string (i.e. no history preserved between sessions). Please note that
257             things might get tricky if that if multiple sessions are running at the same time.
258              
259             =item * -history_size
260              
261             Overwrite the default 100 history entries to save in hisotry_file (if exists).
262              
263             =item * -history_more
264              
265             If the history_file exists, try to load this data from the file during initialization, and save it at loop end.
266             For Example:
267              
268             my %user_defaults ;
269             my $cli = new Term::Shell::MultiCmd ( -history_file => "$ENV{HOME}/.my_saved_data",
270             -history_size => 200,
271             -history_more => \%user_defaults,
272             ) ;
273             # ....
274             $cli -> loop ;
275              
276             This would load shell's history and %user_defaults from the file .my_saved_data before the loop, and
277             store 200 history entries and %user_defaults in the file after the loop.
278              
279             Note that the value of history_more must be a reference for HASH, ARRAY, or SCALAR. And
280             no warnings would be provided if any of the operations fail. It wouldn't be a good idea
281             to use it for sensitive data.
282              
283             =item * -pager
284              
285             As pager's value, this module would expect a string or a sub that returns a FileHandle. If the value is a string,
286             it would be converted to:
287              
288             sub { use FileHandle ; new FileHandle "| $value_of_pager" }
289              
290             When appropriate, the returned file handle would be selected before user's command execution, the old
291             one would be restored afterward. The next example should work on most posix system:
292              
293             my $cli = new Term::Shell::MultiCmd ( -pager => 'less -rX',
294             ...
295              
296             The default pager's value is empty string, which means no pager manipulations.
297              
298             =item * -pager_re
299              
300             Taking after perldb, the default value is '^\|' (i.e. a regular expression that matches '|' prefix, as in
301             the user's command "| help"). If the value is set to an empty string, every command would trigger
302             the pager.
303              
304             The next example would print any output to a given filehandle:
305              
306             my $ret_value ;
307             my $cli = new Term::Shell::MultiCmd ( -pager => sub {
308             open my $fh, '>', \$ret_value or die "can't open FileHandle to string (no PerlIO?)\n" ;
309             $fh
310             },
311             -pager_re => '',
312             ) ;
313             # ...
314             $cli -> cmd ('help -t') ;
315             print "ret_value is:\n $ret_value" ;
316              
317             =item * -record_cmd
318              
319             If it's a function ref, call it with an echo of the user's command
320              
321              
322             my $cli = new Term::Shell::MultiCmd ( -record_cmd => sub {
323             my $user_cmd = shift;
324             system "echo '$user_cmd' >> /tmp/history"
325             }
326             ) ;
327              
328              
329             =item * -empty_cmd
330              
331             Function ref only, call it when user hits 'Return' with no command or args (not even spaces)
332              
333             my $cli = new Term::Shell::MultiCmd ( -empty_cmd => sub {
334             # Assuming some commands are recorded in $last_repeatable_cmd
335             if ( $last_repeatable_cmd ) {
336             # repeat it
337             }
338             }
339             ) ;
340              
341              
342             =item * -query_cmd
343              
344             If exeuting a node, and node contains the query cmd, it would be executed instead of the help command (on the node)
345             Default: 'query'
346             For exmaple, with this feature, if "my cmd query" exists, it would also be exeuted at "my cmd'
347              
348             my $cli = new Term::Shell::MultiCmd ( -query_cmd => 'query',
349             ) ;
350             =item * -enable_sh_pipe
351              
352             If true, allow redirect output to a shell command by the suffix ' | '. Example:
353             Shell> my multy path cmd | grep -w 42
354             Default is value is 1, To disable, set it to false (0 or '' or undef)
355              
356             my $cli = new Term::Shell::MultiCmd ( -enable_sh_pipe => '',
357             ) ;
358              
359             Note: If both pager and this pipe are used, the pipe will be ingored and the command will get whole line
360             as argument.
361              
362             =back
363              
364             =cut
365              
366             sub _new_readline($) {
367 0     0   0 my $o = shift ;
368 4     4   1833 use Term::ReadLine;
  4         7501  
  4         3901  
369 0         0 my $t = eval { local $SIG{__WARN__} = 'IGNORE' ;
  0         0  
370 0         0 Term::ReadLine->new($o->prompt)} ;
371 0 0       0 if (not $t ) {
    0          
    0          
372 0 0       0 die "Can't create Term::ReadLine: $@\n" if -t select ;
373             }
374             elsif (defined $readline::rl_completion_function) {
375             $readline::rl_completion_function =
376 0     0   0 sub { $o -> _complete_cli(@_)} ;
  0         0  
377             }
378             elsif ( defined (my $attr = $t -> Attribs())) {
379             $attr->{attempted_completion_function} =
380             $attr->{completion_function} =
381 0     0   0 sub { $o -> _complete_gnu(@_) } ;
  0         0  
382             }
383             else {
384 0         0 warn __PACKAGE__ . ": no tab completion support for this system. Sorry.\n" ;
385             }
386 0         0 $t
387             }
388              
389             sub new {
390 8     8 1 69922 my $class = shift ;
391 8         10 my $params = 'help_cmd=help quit_cmd=quit root_cmd= prompt=shell>
392             history_file= history_size=100 history_more= pager= pager_re=^\|
393             query_cmd=query enable_sh_pipe=1
394             record_cmd= empty_cmd=
395             ';
396 8         18 my %p = _params $params, @_ ;
397              
398             # structure rules:
399             # hash ref is a path, keys are items (commands or paths) special item $dlm is one liner help
400             # array ref is command's data as [help, command, options, completion]
401             # where: first help line is the one liner, default completion might be good enough
402              
403             my $o = bless { cmds => { },
404 8   33     40 map {($_, $p{$_})} map { /^(\w+)/ } split ' ', $params
  104         172  
  104         141  
405             }, ref ( $class ) || $class ;
406              
407 8         49 $o -> {delimiter } = ' ' ; # now, programmers can manipulate the next two values after creating the object,
408 8         17 $o -> {delimiterRE} = '\s+' ; # but they must be smart enough to read this code. - jezra
409 8         20 $o -> _root_cmds_set() ;
410             # _new_readline $o unless $DB::VERSION ; # Should I add parameter to prevent it?
411             # # it could be useful when coder doesn't plan to use the loop
412             # - on second thought, create it when you have to.
413 8         17 _last_setting_load $o ;
414 8         59 $o
415             }
416              
417             sub _root_cmds_clr($) {
418 0     0   0 my $o = shift ;
419 0         0 my $root = $o->{root};
420 0 0 0     0 return unless $root and $o->{cmds} != $root ;
421 0         0 for ([$o->{help_cmd}, \&_help_command],
422             [$o->{quit_cmd}, \&_quit_command],
423             [$o->{root_cmd}, \&_root_command],
424             ) {
425 0 0 0     0 delete $root->{$_->[0]} if exists $root->{$_->[0]} and $root->{$_->[0]}[1] eq $_->[1]
426             }
427 0         0 delete $o->{root} ;
428 0         0 delete $o->{root_path} ;
429             }
430              
431             sub _root_cmds_set($;$$) {
432 8     8   9 my ($o, $root, $path) = @_ ;
433 8 50       15 ($root, $o->{cmds}) = ($o->{cmds}, $root) if $root ;
434             $o -> add_exec ( path => $o->{help_cmd},
435             exec => \&_help_command,
436             comp => \&_help_command_comp,
437             opts => 'recursive tree',
438             help => 'help [command or prefix]
439             Options:
440             $PATH -t --tree : Show commands tree
441             $PATH -r --recursive : Show full help instead of title, recursively'
442 8 50       56 ) if $o->{help_cmd};
443              
444             $o -> add_exec ( path => $o->{quit_cmd},
445             exec => \&_quit_command,
446             help => 'Exit this shell',
447 8 50       34 ) if $o->{quit_cmd};
448              
449             $o -> add_exec ( path => $o->{root_cmd},
450             exec => \&_root_command,
451             comp => \&_root_command_comp,
452             # opts => 'set display clear', - use its own completion
453             help => 'Execute from, or Set, the root node
454             Usage:
455             $PATH -set a path to node: set the current root at \'a path to node\'
456             $PATH -clear : set the root to real root (alias to -set without parameters)
457             $PATH -display : display the current root (if any)
458             $PATH a path to command -with options
459             : execute command from real root, options would be forwarded
460             : to the command.
461             '
462 8 100       20 ) if $o->{root_cmd};
463 8 50       18 ($o->{root}, $o->{cmds}, $o->{root_path}) = ($o->{cmds}, $root, $path) if $root ;
464             }
465              
466             =head2 add_exec
467              
468             $cli -> add_exec ( -path => 'full command path',
469             -exec => \&my_command,
470             -help => 'some help',
471             -opts => 'options',
472             -comp => \&my_completion_function,
473             ) ;
474              
475             This function adds an command item to the command tree. It is a little complicated, but useful (or so I hope).
476              
477             =over
478              
479             =item * -path
480              
481             B
482             This string would be parsed as multi-words command.
483              
484             Note: by default, this module expects whitespaces delimiter. If you'll read the module's code, you can find
485             an easy way to change it - in unlikely case you'll find it useful.
486              
487             =item * -exec
488              
489             B
490             This code would be called when the user types a unique path for this command (with optional
491             options and arguments). Parameters sent to this code are:
492              
493             my ($cli, %p) = @_ ;
494             # where:
495             # $cli - self object.
496             # $p{ARG0} - the command's full path (user might have used partial but unique path. This is the explicit path)
497             # $p{ARGV} - all user arguments, in order (ARRAY ref)
498             # %p - contains other options (see 'opts' below)
499              
500             =item * -help
501              
502             B
503             The top line would be presented when a one line title is needed (for example, when 'help -tree'
504             is called), the whole string would be presented as the full help for this item.
505              
506             =item * -comp
507              
508             B
509             If Array, when the user hits tab completion for this command, try to complete his input with words
510             from this list.
511             If Hash, using the hash keys as array, following the rule above.
512             If Code, call this function with the next parameters:
513              
514             my ($cli, $word, $line, $start) = @_ ;
515             # where:
516             # $cli is the Term::Shell::MultiCmd object.
517             # $word is the curent word
518             # $line is the whole line
519             # $start is the current location
520              
521             This code should return a list of strings. Term::ReadLine would complete user's line to the longest
522             common part, and display the list (unless unique). In other words - it would do what you expect.
523              
524             For more information, see Term::ReadLine.
525              
526             =item * -opts
527              
528             B
529             If a string, split it to words by whitespaces. Those words are parsed as
530             standard Getopt::Long options. For example:
531              
532             -opts => 'force name=s flag=i@'
533              
534             This would populating the previously described %p hash, correspond to user command:
535              
536             shell> user command -name="Some String" -flag 2 -flag 3 -flag 4 -force
537              
538              
539             For more information, see Getopt::Long. Also see examples/multi_option.pl in distribution.
540              
541             As ARRAY ref, caller can also add a complete 'instruction' after each non-flag option (i.e. an option that
542             expects parameters). Like the 'comp' above, this 'instruction' must be an ARRAY or CODE ref, and follow
543             the same roles. When omitted, a default function would be called and ask the user for input.
544             For example:
545              
546             -opts => [ 'verbose' =>
547             'file=s' => \&my_filename_completion,
548             'level=i' => [qw/1 2 3 4/],
549             'type=s' => \%my_hash_of_types,
550             ],
551              
552             =back
553              
554             =cut
555              
556             sub add_exec {
557 25     25 1 24 my $o = shift ;
558 25         35 my %p = _params 'path exec help= comp= opts=', @_ ;
559 25 50       47 return unless $p{path}; # let user's empty string prevent this command
560 25         25 my $r = $o ->{cmds} ;
561 25         24 my $p = '' ;
562 25 50       45 die "command must be CODE refferance\n" unless 'CODE' eq ref $p{exec} ;
563 25         48 my @w = _split $o, $p{path} ;
564 25 50       52 my $new = pop @w or return ;
565 25         33 for my $w (@w) {
566 8         16 $p .= _join $o, $p, $w ;
567 8 50       17 if ('ARRAY' eq ref $r ->{$w} ) {
568 0         0 carp "Overwrite command '$p'\n" ;
569 0         0 delete $r -> {$w} ;
570             }
571 8   100     32 $r = ($r->{$w} ||= {}) ;
572             }
573 25         31 my ($opts, %opts) = '' ; # now calculate options
574 25 100       43 if ($p{opts}) {
575 8 50       26 my @opts = ref $p{opts} ? @{$p{opts}} : split ' ', $p{opts} ;
  0         0  
576             # croak "options -opts must be ARRAY ref\n" unless 'ARRAY' eq ref $p{opts} ;
577 8         16 while (@opts) {
578 16         17 my $op = shift @opts ;
579 16 50       33 croak "unexpected option completion\n" if ref $op ;
580 16         20 $opts .= "$op " ;
581 16         24 my $expecting = $op =~ s/[\=\:].*$// ;
582 16 0       43 $opts{$op} = ( $expecting ?
    50          
583             ref $opts[0] ?
584             shift @opts :
585             \&_expect_param_comp :
586             '' ) ;
587             }
588             }
589             # 0 1 2 3 4..
590 25         129 $r->{$new} = [@p{qw/help exec comp/}, $opts, %opts]
591             }
592              
593              
594             =head2 add_help
595              
596             Although help string can set in add_exec, this command is useful when he wishes to
597             add title (or hint) to a part of the command path. For example:
598              
599             # assume $cli with commands 'feature set', 'feature get', etc.
600             $cli -> add_help ( -path => 'feature' ,
601             -help => 'This feature is about something') ;
602              
603             =cut
604              
605             sub add_help {
606 1     1 1 7 my $o = shift ;
607 1         4 my %p = _params "path help", @_ ;
608 1         4 my ($cmd, $path, @args, $ret) = _travel $o, $p{path} ; # _split $o, $p{path} ;
609 1 50       6 if ('HASH' eq ref $cmd) {
610 1         3 for my $w (@args) {
611 1         5 $cmd = ($cmd->{$w} = {});
612             }
613             ($ret, $cmd->{$dlm}) = ($cmd->{$dlm}, $p{help})
614 1         6 }
615             else {
616 0 0       0 croak "command '$p{path}' does not exists.\n For sanity reasons, will not add help to non-existing commands\n" if @args;
617             ($ret, $cmd->[0 ]) = ($cmd->[0 ], $p{help})
618 0         0 }
619 1         7 $ret # Was it worth the trouble?
620             }
621              
622             =head2 populate
623              
624             A convenient way to define a chain of add_exec and add_help commands. This function expects hash, where
625             the key is the command path and the value might be HASH ref (calling add_exec), or a string (calling add_help).
626             For example:
627              
628             $cli -> populate
629             ( 'feature' => 'This feature is a secret',
630             'feature set' => { help => 'help for feature set',
631             exec => \&my_feature_set,
632             opts => 'level=i',
633             comp => \&my_feature_set_completion_function,
634             },
635             'feature get' => { help => 'help for feature get',
636             exec => \&my_feature_get
637             },
638             ) ;
639              
640             # Note:
641             # - Since the key is the path, '-path' is omitted from parameters.
642             # - This function returns the self object, for easy chaining (as the synopsis demonstrates).
643              
644             =cut
645              
646             sub populate {
647 8     8 1 16 my ($o, %p) = @_ ;
648 8         26 while (my ($k, $v) = each %p) {
649 9 100       31 if (not ref $v) {
    50          
650 1         25 $o->add_help(-path => $k, -help => $v) ;
651             }
652             elsif ('HASH' eq ref $v) {
653 8         27 $o->add_exec(-path => $k, %$v)
654             }
655             else {
656 0         0 croak "unknow item for '$k': $v\n" ;
657             }
658             }
659             $o
660 8         25 }
661              
662             sub _last_setting_load($) {
663 8     8   11 my $o = shift ;
664 8 100       20 my $f = $o->{history_file} or return ;
665 1 50       147 return unless -f $f ;
666 0         0 my $d = $o->{history_more} ;
667 0         0 eval {
668 4     4   2176 my $setting = eval { use Storable ; retrieve $f } ;
  4         10411  
  4         682  
  0         0  
  0         0  
669 0 0       0 return print "Failed to load configuration from $f: $@\n" if $@ ;
670 0         0 my ($hist, $more) = @$setting ;
671 0 0 0     0 $o->{history_data} = $hist if 'ARRAY' eq ref $hist and @$hist ;
672 0 0 0     0 return unless ref $d and ref $more and ref($d) eq ref($more) ;
      0        
673 0 0       0 %$d = %$more if 'HASH' eq ref $d ;
674 0 0       0 @$d = @$more if 'ARRAY' eq ref $d ;
675 0 0       0 $$d = $$more if 'SCALAR' eq ref $d ;
676             } ;
677             }
678              
679             sub _last_setting_save($) {
680 0     0   0 my $o = shift ;
681 0 0       0 my $f = $o->{history_file} or return ;
682 0         0 my @his = $o -> history();
683 0         0 splice @his, 0, @his - $o->{history_size} ;
684             print
685 4 0   4   18 eval {use Storable ; store ([[@his], $o->{history_more}], $f)} ? # Note: For backward compatibly, this array can only grow
  4         4  
  4         6167  
  0         0  
  0         0  
686             "Configuration saved in $f\n" :
687             "Failed to save configuration in $f: $@\n" ;
688             }
689              
690             =head2 loop
691              
692             $cli -> loop ;
693              
694             Prompt, parse, and invoke in an endless loop
695              
696             ('endless loop' should never be taken literally. Users quit, systems crash, universes collapse -
697             and the loop reaches its last cycle)
698              
699             =cut
700              
701             sub loop {
702 0     0 1 0 local $| = 1 ;
703 0         0 my $o = shift ;
704              
705 0   0     0 $o-> {term} ||= _new_readline $o ;
706 0 0       0 $o-> history($o->{history_data}) if $o->{history_data};
707 0   0     0 while ( not $o -> {stop} and
708             defined (my $line = $o->{term}->readline($o->prompt)) ) {
709 0         0 $o->cmd( $line ) ;
710             }
711 0         0 _last_setting_save $o ;
712             }
713              
714             sub _complete_gnu {
715 0     0   0 my($o, $text, $line, $start, $end) = @_;
716 0         0 $text, &_complete_cli # apparently, this should work
717             }
718              
719             sub _complete_cli {
720 0     0   0 my($o, $word, $line, $start) = @_;
721             # 1. complete command
722             # 2. if current word starts with '-', complete option
723             # 3. if previous word starts with '-', try arg completion
724             # 4. try cmd completion (should it overwrite 3 for default _expect_param_comp?)
725             # 5. show help, keep the line
726              
727             # my @w = _split $o , # should I ignore the rest of the line?
728             # substr $line, 0, $start ; # well, Term::ReadLine expects words list.
729              
730 0         0 my ($cmd, $path, @args) = _travel $o, substr $line, 0, $start ; # @w ;
731 0 0       0 return ($cmd, $word) unless ref $cmd ;
732 0 0       0 return (@args ? "\a" : _filter $word, $cmd) if 'HASH' eq ref $cmd ;
    0          
733              
734 0         0 my ($help, $exec, $comp, $opts, %opts) = @{ $cmd } ; # avoid confusion
  0         0  
735 0 0 0     0 return &_root_command_comp if $comp and $comp == \&_root_command_comp ; # very special case: root 'imports' its options.
736 0 0       0 return map {"$1$_"} _filter $2,\%opts if $word =~ /^(\-\-?)(.*)/ ;
  0         0  
737 0 0 0     0 if ( @args and $args[-1] =~ /^\-\-?(.*)/) {
738 0         0 my ($op, @op) = _filter $1, \%opts ;
739 0 0       0 return ("Option $args[-1] is ambiguous: $op @op?", $word) if @op ;
740 0 0       0 return ("Option $args[-1] is unknown", $word) unless $op ;
741 0         0 my $cb = $opts{$op} ;
742 0 0 0     0 return _filter $word, $cb if 'ARRAY' eq ref $cb or 'HASH' eq ref $cb ;
743 0 0       0 return $cb->($o, $word, $line, $start, $op, $opts =~ /$op(\S*)/ ) if 'CODE' eq ref $cb ;
744             }
745 0 0 0     0 return _filter $word, $comp if 'ARRAY' eq ref $comp or 'HASH' eq ref $comp ;
746 0 0       0 return $comp->($o, $word, $line, $start) if 'CODE' eq ref $comp ;
747 0         0 return ($help, $word) # so be it
748             }
749              
750             sub _help_message_tree { # inspired by Unix 'tree' command
751             # Should I add ANSI colors?
752 0     0   0 my ($h, $cmd, $pre, $last) = @_ ;
753 0 0       0 print $pre . ($last ? '`' : '|') if $pre ;
    0          
754 0 0       0 return _say "- $cmd : ", $h->[0] =~ /^(.*)/m if 'ARRAY' eq ref $h ;
755 0         0 _say "-- $cmd" ;
756 0         0 my @c = sort keys %$h ;
757 0         0 for my $c (@c) {
758 0 0 0     0 _help_message_tree( $h->{$c},
    0          
    0          
759             $c,
760             $pre ? $pre . ($last ? ' ' : '| ') : ' ' ,
761             $c eq ($c[-1]||'')
762             ) unless $c eq $dlm ;
763             }
764             }
765              
766             sub _help_message {
767 0     0   0 my $o = shift ;
768 0         0 my %p = _params "node path full= recursive= tree= ARGV= ARG0=", @_ ;
769 0         0 my ($h, $p) = @p{'node', 'path'} ;
770 0         0 $p =~ s/^\s*(.*?)\s*$/$1/ ;
771             sub _align2($$) {
772 0     0   0 my ($a, $b) = @_;
773 0         0 _say $a, (' ' x (20 - length $a)), ': ', $b
774             }
775              
776 0 0       0 if ('ARRAY' eq ref $h) { # simple command, full help
    0          
    0          
    0          
    0          
777 0         0 my $help = $h->[0] ;
778 0         0 $help =~ s/\$PATH/$p{path}/g ;
779 0         0 _say "$p:\n $help" ;
780 0         0 $help
781             }
782             elsif ('HASH' ne ref $h) { # this one shouldn't happen
783 0         0 confess "bad item in help message: $h"
784             }
785             elsif ($p{recursive}) { # show everything
786 0         0 my $xxx = "----------------------\n" ;
787 0 0       0 _say $xxx, $p, ":\t", $h->{$dlm} if exists $h->{$dlm};
788              
789 0         0 for my $k (sort keys %$h) {
790 0 0       0 next if $k eq $dlm ;
791 0         0 _say $xxx ;
792 0         0 _help_message( $o, %p, -node => $h->{$k}, -path => _join $o, $p, $k) ;
793             }
794             }
795             elsif ($p{tree}) { # tree - one linear for each one
796 0         0 _help_message_tree ($h, $p)
797             }
798             elsif ($p{full}) { # prefix, full list
799              
800 0 0       0 _say "$p:\t", $h->{$dlm} if exists $h->{$dlm} ;
801              
802 0         0 for my $k (sort keys %$h) {
803 0 0       0 next if $k eq $dlm ;
804             my ($l) = (('ARRAY' eq ref $h->{$k}) ?
805             ($h->{$k}[0] || 'a command') :
806 0 0 0     0 ($h->{$k}{$dlm} || 'a prefix' ) ) =~ /^(.*)$/m ;
      0        
807 0         0 _align2 _join($o, $p, $k), $l;
808             }
809             }
810             else { # just show the prefix with optional help
811 0   0     0 _say "$p: \t", $h->{$dlm} || 'A command prefix' ;
812             }
813             }
814              
815             sub _help_command {
816 0     0   0 my ($o, %p) = @_ ;
817 0         0 my ($cmd, $path, @args) = _travela $o, @{$p{ARGV}} ;
  0         0  
818 0 0       0 return _say $cmd unless ref $cmd ;
819 0 0       0 return _say "No such command or prefix: " . _join $o, $path, @args if @args ;
820 0         0 return _help_message($o, -node => $cmd, -path => $path, -full => 1, %p) ;
821             }
822              
823             sub _help_command_comp {
824 0     0   0 my($o, $word, $line, $start) = @_;
825 0         0 my @w = _split $o , substr $line, 0, $start ;
826 0         0 shift @w ;
827 0         0 my ($cmd, $path, @args) = _travela $o, grep {!/\-\-?r(?:ecursive)?|\-\-?t(?:ree)?/} @w ;
  0         0  
828             # potential issue: 'help -r some path' wouldn't be a valid path, is DWIM the solution?
829 0 0       0 return ($cmd, $word) unless ref $cmd ;
830 0 0       0 return _filter $word, $cmd if 'HASH' eq ref $cmd ;
831 0         0 ('', $word)
832             }
833              
834 0     0   0 sub _quit_command { $_[0]->{stop} = 1 }
835              
836             sub _root_command_comp {
837 0     0   0 my($o, $word, $line, $start) = @_;
838 0         0 $line =~ s/^(\s*\S+\s*(?:(\-\-?)(\w*))?)// ; # todo: delimiterRE
839 0         0 my ($prolog, $par, $param) = ($1, $2, $3) ;
840 0 0       0 return unless $prolog ; # error, avoid recursion
841 0 0 0     0 return map {"$par$_"} _filter $param, qw/clear set display/ if $par and not $line ;
  0         0  
842 0         0 $line =~ s/^(\s*)// ;
843 0         0 $prolog .= $1 ;
844 0         0 my $root = delete $o -> {root} ;
845 0         0 my @res = _complete_cli($o, $word, $line, $start - length $prolog) ;
846 0 0       0 $o->{root} = $root if $root ;
847             @res
848 0         0 }
849              
850             sub _root_command {
851             # root -display : display current path
852             # root -set path : set path
853             # root -clear : alias to root -set (without a path)
854             # root path params: execute path from real command root
855              
856 0     0   0 my ($o, %p) = @_ ;
857 0         0 my @argv = @{$p{ARGV}} ;
  0         0  
858 0 0       0 @argv or return $o->cmd("help $p{ARG0}") ;
859             # algo: can't parse those options automaticaly, as it would prevent user's options to optional root commnad
860 0 0       0 $argv[0] =~ /^\-\-?d/ and return _say $o->{root} ? "root is set to '$o->{root_path}'" : "root is clear." ;
    0          
861 0 0       0 $argv[0] =~ /^\-\-?c/ and @argv = ('-set') ;
862 0 0       0 $argv[0] =~ /^\-\-?s/ or do {
863             # just do it, do it!
864 0         0 my $root = delete $o->{root} ;
865 0         0 my @res = $o->cmd(_join $o, @argv) ;
866 0 0       0 $o->{root} = $root if $root ;
867 0         0 return @res ;
868             } ;
869 0         0 shift @argv ; # -set, it is
870 0         0 my ($cmd, $path, @args) ;
871 0 0       0 if (@argv) {
872 0         0 my $root = delete $o->{root} ;
873 0         0 ($cmd, $path, @args) = _travela $o, @argv ;
874 0 0       0 $o->{root} = $root if $root ;
875 0 0       0 return _say $cmd unless ref $cmd ;
876 0 0       0 return _say "No such prefix: " . _join $o, $path, @args if @args ;
877 0 0       0 return _say "$path: is a command. Only a node can be set as root." if 'ARRAY' eq ref $cmd ;
878             }
879 0 0       0 if ( $o->{root}) {
880 0         0 _say "clear root '$o->{root_path}'" ;
881 0         0 _root_cmds_clr $o ;
882             }
883 0 0       0 if ( $cmd ) {
884 0         0 _root_cmds_set $o, $cmd, $path ;
885 0         0 _say "set new root: '$path'" ;
886             }
887             }
888              
889             sub _check_sh_pipe {
890 8     8   10 my ($o, $c) = @_ ;
891 8         23 my $r = qr/(\|.*)$/;
892 8 50       30 if ($c =~ s/$r//) {
893 0         0 my $cmd = $1;
894 0         0 $o->{piper} = 'c';
895 4     4   1598 $o->{shcmd} = sub { use FileHandle ; new FileHandle $cmd };
  4     0   30612  
  4         17  
  0         0  
  0         0  
896             }
897 8         25 ($o, $c)
898             }
899              
900             sub _check_pager {
901 8     8   9 my ($o, $c) = @_ ;
902 8 50       35 my $p = $o->{pager} or return (@_, $o->{piper}=undef); # just in case programmer delete {pager} during run
903 0         0 my $r = $o->{pager_re};
904 0 0 0     0 if ($r and not ref $r) { # once
905 0         0 my $d = "$r($o->{delimiterRE})*" ;
906 0         0 $r = $o->{pager_re} = qr/$d/;
907             }
908 0 0 0     0 if (!$r or
      0        
909             $r && $c =~ s/$r//) {
910 0         0 $o->{piper} = 'p';
911 4 0   4   1659 $o->{pager} = sub { use FileHandle ; new FileHandle "| $p" } unless ref $o->{pager};
  4     0   6  
  4         12  
  0         0  
  0         0  
912             }
913 0         0 ($o, $c)
914             }
915              
916             sub _check_silent_aliases {
917 8     8   7 my ($o, $cmd) = @_ ;
918 8 50       20 return $cmd unless $cmd;
919 8   33     23 my $r = $o->{root} || $o->{cmds};
920 8   50     22 my ($c, @a) = _split $o, $cmd || '';
921              
922             return _join $o, $o->{root_cmd}, (@a ? (-set => @a ) : ('-clear'))
923             if ( $c eq 'cd' and
924             $o->{root_cmd} and
925 8 0 33     39 not exists $r->{cd});
    0 0        
926              
927             return _join $o, $o->{help_cmd}, @a
928             if $o->{help_cmd} and
929             ( ($c eq 'ls' and not exists $r->{ls} ) or
930 8 50 33     54 ($c eq 'help' and not exists $r->{help}) );
      33        
931              
932 8         14 $cmd
933             }
934              
935             =head2 cmd
936              
937             $cli -> cmd ( "help -tree" ) ;
938              
939             Execute the given string parameter, similarly to user input. This one might be useful to execute
940             commands in a script, or testing.
941              
942             =cut
943              
944             sub cmd {
945 7     7 1 5 my ($o, $clt) = @_;
946 7 100       18 $o->{record_cmd}->($clt) if 'CODE' eq ref $o->{record_cmd};
947              
948 7 50       180 my ($cmd, $path, @args) = _travel $o, $clt or return ;
949 7         14 local %SIG ;
950              
951 7         4 my $fh;
952 7 50 50     28 $fh = $o->{pager}->() if 'p' eq ($o->{piper}||'');
953 7 50 50     40 $fh = $o->{shcmd}->() if 'c' eq ($o->{piper}||'') and not $fh;
      33        
954 7 50       11 if ($fh) {
955 0         0 $o->{stdout} = select ;
956 0         0 select $fh ;
957 0     0   0 $SIG{PIPE} = sub {} ;
958             }
959              
960 7         16 my $res = $o->_cmd ($cmd, $path, @args) ;
961              
962 7 50       25 if ($fh) {
963 0         0 select $o->{stdout} ;
964 0         0 $o->{piper} = $o->{shcmd} = undef;
965             }
966             $res
967 7         37 }
968              
969             sub _cmd {
970 7     7   8 my ($o, $cmd, $path, @args) = @_ ;
971 7 50       11 return print $cmd unless ref $cmd ;
972 7 50 33     39 return $o->{empty_cmd}->() if $o->{empty_cmd} and $cmd eq ($o -> {root} || $o->{cmds}) and 0 == length join '', @args;
      66        
      33        
973 7 50       17 return _say "No such command or prefix: " . _join $o, @args if $cmd eq $o->{cmds} ;
974 7 0 33     14 $cmd = $cmd->{$o->{query_cmd}} if 'HASH' eq ref $cmd and length($o->{query_cmd}) and exists $cmd->{$o->{query_cmd}};
      33        
975 7 50       14 return _help_message($o, -node => $cmd, -path => $path) unless 'ARRAY' eq ref $cmd ; # help message
976 7   50     26 my %p = _options $cmd->[3] || '', @args ;
977 7 50       13 return print $p{_ERR_} if $p{_ERR_} ;
978 7         19 return $cmd->[1]->($o, ARG0 => $path, %p) ;
979             }
980              
981             =head2 command
982              
983             $cli -> command ( "help -tree") ;
984             Is the same as cmd, but echos the command before execution
985              
986             =cut
987              
988             sub command {
989 0     0 1   my ($o, $cmd) = @_ ;
990 0           print "$cmd ..\n" ;
991 0           &cmd
992             }
993              
994             =head2 complete
995              
996             my ($base_line, @word_list) = $cli -> complete ($a_line) ;
997              
998             given a line, this function would return a base line (i.e. truncated to the beginning of the last word), and a list of potential
999             completions. Added to the 'cmd' command, this might be useful when module user implements his own 'loop' command in a non-terminal
1000             application
1001              
1002             =cut
1003              
1004             sub complete {
1005             # line, pos ==> line, list of words
1006 0     0 1   my ($o, $line, $pos) = @_ ;
1007 0 0         my $lo = substr $line, $pos, -1, '' if defined $pos ;
1008 0           my $lu = $line ;
1009 0           my $qd = $o -> {delimiterRE} ;
1010 0           $lu =~ s/([^$qd]*)$// ;
1011 0   0       my $w = $1 || '' ;
1012 0   0       my (@list) = _complete_cli($o, $w, $line, $pos || length $lu) ;
1013             # if ($lu =~ /^(.*)($qd+)$/) {
1014             # # this is duplicating what is done in _complete_cli, TODO: optimize
1015             # my ($l, $s) = ($1, $2 ) ;
1016             # my ($cmd, $path, @args) = _travel $o, $l ;
1017             # $lu = "$path$s" if $path and not @args ;
1018             # }
1019 0           ($lu, @list)
1020             }
1021              
1022             =head2 prompt
1023              
1024             my $prompt = $cli -> prompt() ;
1025              
1026             accepts no parameters, return current prompt.
1027              
1028             =cut
1029              
1030              
1031             sub prompt() {
1032 0     0 1   my $o = shift ;
1033 0   0       my $p = $o->{prompt} || 'shell' ;
1034 0 0         return $p->() if 'CODE' eq ref $p ;
1035 0 0         return $p if $p =~ /\W$/ ;
1036 0 0         $p .= ':' . $o->{root_path} if $o->{root_path} ;
1037 0           $p . '> '
1038             }
1039              
1040             =head2 history
1041              
1042             set/get history
1043              
1044             my @hist = $cli -> history() ; # get history
1045             $cli -> history( @alternative_history ) ; # set history
1046             $cli -> history([@alternative_history]) ; # the very same, by ptr
1047             $cli -> history([]) ; # clear history
1048              
1049             =cut
1050              
1051             sub history {
1052 0     0 1   my $o = shift ;
1053 0 0         return unless $o->{term} ;
1054 0 0         return $o->{term}->SetHistory(map {('ARRAY' eq ref $_) ? (@$_) : ($_)} @_ ) if @_ ;
  0 0          
1055             return $o->{term}->GetHistory
1056 0           }
1057              
1058              
1059             # =head2 pager
1060              
1061             # my $old_pager = $o->pager($new_pager); # set new pager
1062             # my $old_pager = $o->pager('') ; # clear pager
1063             # my $cur_pager = $o->pager() ; # keep current pager
1064              
1065             # =cut
1066              
1067             # sub pager {
1068             # my ($o, $new) = @_ ;
1069             # my $old = $o->{pager} ;
1070             # $o->{pager} = $new if defined $new ;
1071             # $old
1072             # }
1073              
1074             =head1 ALSO SEE
1075              
1076             Term::ReadLine, Term::ReadKey, Getopt::Long
1077              
1078             =head1 AUTHOR
1079              
1080             Josef Ezra, C<< >>
1081              
1082             =head1 BUGS
1083              
1084             Please report any bugs or feature requests to me, or to C, or through
1085             the web interface at L.
1086             I am grateful for your feedback.
1087              
1088             =head2 TODO list
1089              
1090             nImplement pager.
1091              
1092             =head1 SUPPORT
1093              
1094             You can find documentation for this module with the perldoc command.
1095              
1096             perldoc Term::Shell::MultiCmd
1097              
1098             You can also look for information at:
1099              
1100             =over 4
1101              
1102             =item * RT: CPAN's request tracker
1103              
1104             L
1105              
1106             =item * AnnoCPAN: Annotated CPAN documentation
1107              
1108             L
1109              
1110             =item * CPAN Ratings
1111              
1112             L
1113              
1114             =item * Search CPAN
1115              
1116             L
1117              
1118             =back
1119              
1120              
1121             =head1 ACKNOWLEDGMENTS
1122              
1123             This module was inspired by the excellent modules Term::Shell, CPAN, and CPANPLUS::Shell.
1124              
1125             =head1 LICENSE AND COPYRIGHT
1126              
1127             Copyright 2010 Josef Ezra.
1128              
1129             This program is free software; you can redistribute it and/or modify it
1130             under the terms of either: the GNU General Public License as published
1131             by the Free Software Foundation; or the Artistic License.
1132              
1133             See http://dev.perl.org/licenses/ for more information.
1134              
1135              
1136             =cut
1137              
1138             'end'
1139