File Coverage

blib/lib/Lim/CLI.pm
Criterion Covered Total %
statement 45 304 14.8
branch 0 132 0.0
condition 0 18 0.0
subroutine 15 33 45.4
pod 12 12 100.0
total 72 499 14.4


line stmt bran cond sub pod time code
1             package Lim::CLI;
2              
3 2     2   11022 use common::sense;
  2         6  
  2         38  
4 2     2   114 use Carp;
  2         7  
  2         174  
5              
6 2     2   1544 use Log::Log4perl ();
  2         72295  
  2         57  
7 2     2   16 use Scalar::Util qw(blessed weaken);
  2         5  
  2         204  
8 2     2   3560 use Module::Find qw(findsubmod);
  2         2957  
  2         135  
9 2     2   15 use Fcntl qw(:seek);
  2         4  
  2         307  
10 2     2   1384 use File::Temp ();
  2         24308  
  2         52  
11 2     2   991 use IO::File ();
  2         1174  
  2         49  
12 2     2   1161 use Digest::SHA ();
  2         5084  
  2         52  
13              
14 2     2   19 use Lim ();
  2         4  
  2         36  
15 2     2   696 use Lim::Error ();
  2         6  
  2         46  
16 2     2   644 use Lim::Agent ();
  2         6  
  2         43  
17 2     2   1430 use Lim::Plugins ();
  2         8  
  2         59  
18              
19 2     2   23 use IO::Handle ();
  2         6  
  2         34  
20 2     2   12 use AnyEvent::Handle ();
  2         4  
  2         9979  
21              
22             =encoding utf8
23              
24             =head1 NAME
25              
26             Lim::CLI - The command line interface to Lim
27              
28             =head1 VERSION
29              
30             See L for version.
31              
32             =cut
33              
34             our $VERSION = $Lim::VERSION;
35             our @BUILTINS = (qw(quit exit help));
36              
37             =head1 SYNOPSIS
38              
39             =over 4
40              
41             use Lim::CLI;
42              
43             $cli = Lim::CLI->new(...);
44              
45             =back
46              
47             =head1 DESCRIPTION
48              
49             This is the CLI that takes the input from the user and sends it to the plugin in
50             question. It uses L if it is available and that enables
51             command line completion and history functions. It will load all plugins present
52             on the system and use their CLI part if it exists.
53              
54             Failing to have a supported readline module it will use a basic
55             L to read each line of input and process it.
56              
57             Built in commands that can not be used by any plugins are:
58              
59             =over 4
60              
61             quit - Will quit the CLI
62             exit - Will exit the relative section or quit the CLI
63             help - Will show help for the relative section where the user is
64              
65             =back
66              
67             =head1 METHODS
68              
69             =over 4
70              
71             =item $cli = Lim::CLI->new(key => value...)
72              
73             Create a new Lim::CLI object.
74              
75             =over 4
76              
77             =item on_quit => $callback->($cli_object)
78              
79             Callback to call when the CLI quits, either with the user doing CTRL-D, CTRL-C
80             or the command 'quit'.
81              
82             =back
83              
84             =cut
85              
86             sub new {
87 0     0 1   my $this = shift;
88 0   0       my $class = ref($this) || $this;
89 0           my %args = ( @_ );
90 0           my $self = {
91             logger => Log::Log4perl->get_logger,
92             cli => {},
93             busy => 0,
94             no_completion => 0,
95             prompt => 'lim> '
96             };
97 0           bless $self, $class;
98 0           my $real_self = $self;
99 0           weaken($self);
100              
101 0 0         unless (defined $args{on_quit}) {
102 0           confess __PACKAGE__, ': Missing on_quit';
103             }
104 0 0         unless (ref($args{on_quit}) eq 'CODE') {
105 0           confess __PACKAGE__, ': on_quit is not CODE';
106             }
107 0           $self->{on_quit} = $args{on_quit};
108              
109 0           foreach my $module (qw(Lim::Agent)) {
110 0           my $name = lc($module->Name);
111            
112 0 0         if (exists $self->{cli}->{$name}) {
113 0 0         Lim::WARN and $self->{logger}->warn('Can not load internal CLI module ', $module, ': name ', $name, ' already in use');
114 0           next;
115             }
116              
117 0 0         if (defined (my $obj = $module->CLI(cli => $self))) {
118 0           $self->{cli}->{$name} = {
119             name => $name,
120             module => $module,
121             obj => $obj
122             };
123             }
124             }
125            
126 0           foreach my $module (Lim::Plugins->instance->LoadedModules) {
127 0           my $name = lc($module->Name);
128            
129 0 0         if (exists $self->{cli}->{$name}) {
130 0 0         Lim::WARN and $self->{logger}->warn('Can not use CLI module ', $module, ': name ', $name, ' already in use');
131 0           next;
132             }
133            
134 0 0         if (defined (my $obj = $module->CLI(cli => $self))) {
135 0           $self->{cli}->{$name} = {
136             name => $name,
137             module => $module,
138             obj => $obj
139             };
140             }
141             }
142            
143 0           eval {
144 0           require AnyEvent::ReadLine::Gnu;
145             };
146 0 0         unless ($@) {
147             $self->{rl} = AnyEvent::ReadLine::Gnu->new(
148             prompt => 'lim> ',
149             on_line => sub {
150 0 0   0     unless (defined $self) {
151 0           return;
152             }
153            
154 0           $self->process(@_);
155 0           });
156            
157 0           $self->{rl}->Attribs->{completion_entry_function} = $self->{rl}->Attribs->{list_completion_function};
158             $self->{rl}->Attribs->{attempted_completion_function} = sub {
159 0     0     my ($text, $line, $start, $end) = @_;
160              
161 0 0         unless (defined $self) {
162 0           return;
163             }
164            
165 0           my @parts = split(/\s+/o, substr($line, 0, $start));
166 0           my $builtins = 0;
167            
168 0 0         if ($self->{current}) {
169 0           unshift(@parts, $self->{current}->{name});
170 0           $builtins = 1;
171             }
172            
173 0 0         if (scalar @parts) {
174 0           my $part = shift(@parts);
175            
176 0 0         if (exists $self->{cli}->{$part}) {
177 0           my $cmd = $self->{cli}->{$part}->{module}->Commands;
178            
179 0           while (defined ($part = shift(@parts))) {
180 0 0 0       unless (exists $cmd->{$part} and ref($cmd->{$part}) eq 'HASH') {
181 0 0         if ($self->{no_completion}++ == 2) {
182 0 0         if (ref($cmd->{$part}) eq 'ARRAY') {
183 0 0         if (@{$cmd->{$part}} == 1) {
  0 0          
  0            
184 0           $self->println('completion finished: ', $part, ' - ', $cmd->{$part}->[0]);
185             }
186             elsif (@{$cmd->{$part}} == 2) {
187 0           $self->println('completion finished: ', $part, ' ', $cmd->{$part}->[0], ' - ', $cmd->{$part}->[1]);
188             }
189             else {
190 0           $self->println('no completion found');
191             }
192             }
193             else {
194 0           $self->println('no completion found');
195             }
196             }
197 0           $self->{rl}->Attribs->{completion_word} = [];
198 0           return ();
199             }
200            
201 0           $builtins = 0;
202 0           $cmd = $cmd->{$part};
203             }
204 0 0         if ($builtins) {
205 0           $self->{rl}->Attribs->{completion_word} = [keys %{$cmd}, @BUILTINS];
  0            
206             }
207             else {
208 0           $self->{rl}->Attribs->{completion_word} = [keys %{$cmd}];
  0            
209             }
210             }
211             else {
212 0 0         if ($self->{no_completion}++ == 2) {
213 0           $self->println('no completion found');
214             }
215 0           $self->{rl}->Attribs->{completion_word} = [];
216 0           return;
217             }
218             }
219             else {
220 0           $self->{rl}->Attribs->{completion_word} = [keys %{$self->{cli}}, @BUILTINS];
  0            
221             }
222 0           $self->{no_completion} = 0;
223 0           return ();
224 0           };
225              
226 0           $self->{rl}->StifleHistory(Lim::Config->{cli}->{history_length});
227 0 0 0       if (Lim::Config->{cli}->{history_file} and -r Lim::Config->{cli}->{history_file}) {
228 0           $self->{rl}->ReadHistory(Lim::Config->{cli}->{history_file});
229 0           $self->{rl}->history_set_pos($self->{rl}->Attribs->{history_length});
230             }
231             }
232             else {
233             $self->{stdin_watcher} = AnyEvent::Handle->new(
234             fh => \*STDIN,
235             on_error => sub {
236 0     0     my ($handle, $fatal, $msg) = @_;
237 0           $handle->destroy;
238 0 0         unless (defined $self) {
239 0           return;
240             }
241 0           $self->{on_quit}($self);
242             },
243             on_eof => sub {
244 0     0     my ($handle) = @_;
245 0           $handle->destroy;
246 0 0         unless (defined $self) {
247 0           return;
248             }
249 0           $self->{on_quit}($self);
250             },
251             on_read => sub {
252 0     0     my ($handle) = @_;
253              
254             $handle->push_read(line => sub {
255 0           shift;
256 0 0         unless (defined $self) {
257 0           return;
258             }
259 0           $self->process(@_);
260 0           });
261 0           });
262            
263 0           IO::Handle::autoflush STDOUT 1;
264             }
265              
266 0 0         if (defined (my $appender = Log::Log4perl->appender_by_name('LimCLI'))) {
267 0           Log::Log4perl->eradicate_appender('Screen');
268 0           $appender->{cli} = $self;
269 0           weaken($appender->{cli});
270             }
271            
272 0           $self->println('Welcome to LIM ', $Lim::VERSION, ' command line interface');
273 0           $self->prompt;
274              
275 0 0         Lim::OBJ_DEBUG and $self->{logger}->debug('new ', __PACKAGE__, ' ', $self);
276 0           $real_self;
277             }
278              
279             sub DESTROY {
280 0     0     my ($self) = @_;
281 0 0         Lim::OBJ_DEBUG and $self->{logger}->debug('destroy ', __PACKAGE__, ' ', $self);
282            
283 0 0         if (exists $self->{rl}) {
284 0 0         if (Lim::Config->{cli}->{history_file}) {
285 0           $self->{rl}->WriteHistory(Lim::Config->{cli}->{history_file});
286             }
287             }
288            
289 0           delete $self->{current};
290 0           delete $self->{rl};
291 0           delete $self->{stdin_watcher};
292 0           delete $self->{cli};
293             }
294              
295             =item $cli->process($line)
296              
297             Process a line of input, called from the input watcher
298             (L or L).
299              
300             =cut
301              
302             sub process {
303 0     0 1   my ($self, $line) = @_;
304 0           my ($cmd, $args);
305            
306 0 0         if ($self->{busy}) {
307 0           return;
308             }
309              
310 0 0         if (defined $line) {
311 0           ($cmd, $args) = split(/\s+/o, $line, 2);
312 0           $cmd = lc($cmd);
313             }
314             else {
315 0           $cmd = 'quit';
316             }
317            
318 0 0         if ($cmd eq 'quit') {
    0          
    0          
319 0           $self->{on_quit}($self);
320 0           return;
321             }
322             elsif ($cmd eq 'exit') {
323 0 0         if (exists $self->{current}) {
324 0           delete $self->{current};
325 0           $self->set_prompt('lim> ');
326 0           $self->prompt;
327             }
328             else {
329 0           $self->{on_quit}($self);
330 0           return;
331             }
332             }
333             elsif ($cmd eq 'help') {
334 0 0         if (exists $self->{current}) {
335 0           $self->print_command_help($self->{current}->{module}->Commands);
336             }
337             else {
338 0           my @cmds = keys %{$self->{cli}};
  0            
339 0           push(@cmds, @BUILTINS);
340 0           $self->println('Available commands: ', join(' ', sort @cmds));
341             }
342 0           $self->prompt;
343             }
344             else {
345 0 0         if ($cmd) {
346 0 0         if (exists $self->{current}) {
    0          
347 0 0 0       if ($self->{current}->{module}->Commands->{$cmd} and
348             $self->{current}->{obj}->can($cmd))
349             {
350 0           $self->{busy} = 1;
351 0           $self->set_prompt('');
352 0           $self->{current}->{obj}->$cmd($args);
353             }
354             else {
355 0           $self->unknown_command($cmd);
356             }
357             }
358             elsif (exists $self->{cli}->{$cmd}) {
359 0 0         if ($args) {
360 0           my $current = $self->{cli}->{$cmd};
361 0           ($cmd, $args) = split(/\s+/o, $args, 2);
362 0           $cmd = lc($cmd);
363            
364 0 0 0       if ($current->{module}->Commands->{$cmd} and
365             $current->{obj}->can($cmd))
366             {
367 0           $self->{busy} = 1;
368 0           $self->set_prompt('');
369 0           $current->{obj}->$cmd($args);
370             }
371             else {
372 0           $self->unknown_command($cmd);
373             }
374             }
375             else {
376 0           $self->{current} = $self->{cli}->{$cmd};
377 0           $self->set_prompt('lim'.$self->{current}->{obj}->Prompt.'> ');
378 0           $self->prompt;
379             }
380             }
381             else {
382 0           $self->unknown_command($cmd);
383             }
384             }
385             else {
386 0           $self->prompt;
387             }
388             }
389             }
390              
391             =item $cli->prompt
392              
393             Print the prompt, called from C.
394              
395             =cut
396              
397             sub prompt {
398 0     0 1   my ($self) = @_;
399            
400 0 0         if (exists $self->{rl}) {
401 0           return;
402             }
403            
404 0           $self->print($self->{prompt});
405 0           IO::Handle::flush STDOUT;
406             }
407              
408             =item $cli->set_prompt
409              
410             Set the prompt, called from C.
411              
412             =cut
413              
414             sub set_prompt {
415 0     0 1   my ($self, $prompt) = @_;
416            
417 0           $self->{prompt} = $prompt;
418              
419 0 0         if (exists $self->{rl}) {
420 0           $self->{rl}->hide;
421 0           $AnyEvent::ReadLine::Gnu::prompt = $prompt;
422 0           $self->{rl}->show;
423             }
424            
425 0           $self;
426             }
427              
428             =item $cli->clear_line
429              
430             Reset the input.
431              
432             =cut
433              
434             sub clear_line {
435 0     0 1   my ($self) = @_;
436              
437 0 0         if (exists $self->{rl}) {
438 0           $self->{rl}->replace_line('', 1);
439 0           $self->{rl}->hide;
440 0           $self->{rl}->show;
441             }
442             else {
443 0           $self->{stdin_watcher}->{rbuf} = '';
444 0           print "\r";
445 0           IO::Handle::flush STDOUT;
446             }
447            
448 0           $self;
449             }
450              
451             =item $cli->unknown_command
452              
453             Prints the "unknown command" error if the command can not be found.
454              
455             =cut
456              
457             sub unknown_command {
458 0     0 1   my ($self, $cmd) = @_;
459            
460 0           $self->println('unknown command: ', $cmd);
461 0           $self->prompt;
462            
463 0           $self;
464             }
465              
466             =item $cli->print
467              
468             Print some output, called from L and here.
469              
470             =cut
471              
472             sub print {
473 0     0 1   my $self = shift;
474            
475 0 0         if (exists $self->{rl}) {
476 0           $self->{rl}->print(@_);
477             }
478             else {
479 0           foreach (@_) {
480 0           print;
481 0           IO::Handle::flush STDOUT;
482             }
483             }
484            
485 0           $self;
486             }
487              
488             =item $cli->println
489              
490             Print some output and add a newline, called from L and
491             here.
492              
493             =cut
494              
495             sub println {
496 0     0 1   my $self = shift;
497            
498 0 0         if (exists $self->{rl}) {
499 0           $self->{rl}->hide;
500 0           $self->{rl}->print(@_, "\n");
501 0           $self->{rl}->show;
502             }
503             else {
504 0           foreach (@_) {
505 0           print;
506 0           IO::Handle::flush STDOUT;
507             }
508 0           print "\n";
509 0           IO::Handle::flush STDOUT;
510             }
511              
512 0           $self;
513             }
514              
515             =item $cli->print_command_help($module->Commands)
516              
517             Print the help for all commands from a plugin.
518              
519             =cut
520              
521             sub print_command_help {
522 0     0 1   my ($self, $commands, $level) = @_;
523 0           my $space = ' ' x ($level * 4);
524            
525 0 0         if (ref($commands) eq 'HASH') {
526 0           foreach my $key (sort (keys %$commands)) {
527 0 0         if (ref($commands->{$key}) eq 'HASH') {
    0          
528 0           $self->println($space, $key);
529 0           $self->print_command_help($commands->{$key}, $level+1);
530             }
531             elsif (ref($commands->{$key}) eq 'ARRAY') {
532 0 0         if (@{$commands->{$key}} == 1) {
  0 0          
  0            
533 0           $self->println($space, $key, ' - ', $commands->{$key}->[0]);
534             }
535             elsif (@{$commands->{$key}} == 2) {
536 0           $self->println($space, $key, ' ', $commands->{$key}->[0], ' - ', $commands->{$key}->[1]);
537             }
538             else {
539 0           $self->println($space, $key, ' - unknown/invalid help');
540             }
541             }
542             else {
543 0           $self->println($space, $key, ' - no help');
544             }
545             }
546             }
547            
548 0           $self;
549             }
550              
551             =item $cli->Successful
552              
553             Called from L when a command was successful.
554              
555             =cut
556              
557             sub Successful {
558 0     0 1   my ($self) = @_;
559            
560 0           $self->{busy} = 0;
561 0 0         if (exists $self->{current}) {
562 0           $self->set_prompt('lim'.$self->{current}->{obj}->Prompt.'> ');
563             }
564             else {
565 0           $self->set_prompt('lim> ');
566             }
567 0           $self->prompt;
568 0           return;
569             }
570              
571             =item $cli->Error($LimError || @error_text)
572              
573             Called from L when a command issued an error. The error can
574             be a L object or list of strings that will be joined to produce an
575             error string.
576              
577             =cut
578              
579             sub Error {
580 0     0 1   my $self = shift;
581            
582 0 0         $self->print('Command Error: ', ( scalar @_ > 0 ? '' : 'unknown' ));
583 0           foreach (@_) {
584 0 0 0       if (blessed $_ and $_->isa('Lim::Error')) {
585 0           $self->print($_->toString);
586             }
587             else {
588 0           $self->print($_);
589             }
590             }
591 0           $self->println;
592            
593 0           $self->{busy} = 0;
594 0 0         if (exists $self->{current}) {
595 0           $self->set_prompt('lim'.$self->{current}->{obj}->Prompt.'> ');
596             }
597             else {
598 0           $self->set_prompt('lim> ');
599             }
600 0           $self->prompt;
601             }
602              
603             =item $cli->Editor($content)
604              
605             Call up an editor for the C<$content> provided. Will return the new content if
606             it has changed or undef on error or if nothing was changed.
607              
608             Will use L->{cli}->{editor} which will be the environment variable
609             EDITOR or what ever your configure it to be.
610              
611             =cut
612              
613             sub Editor {
614 0     0 1   my ($self, $content) = @_;
615 0           my $tmp = File::Temp->new;
616 0           my $sha = Digest::SHA::sha1_base64($content);
617            
618 0 0         Lim::DEBUG and $self->{logger}->debug('Editing ', $tmp->filename, ', hash before ', $sha);
619            
620 0           print $tmp $content;
621 0           $tmp->flush;
622              
623             # TODO check if editor exists
624            
625 0 0         if (system(Lim::Config->{cli}->{editor}, $tmp->filename)) {
626 0 0         Lim::DEBUG and $self->{logger}->debug('EDITOR returned failure');
627 0           return;
628             }
629              
630 0           my $fh = IO::File->new;
631 0 0         unless ($fh->open($tmp->filename)) {
632 0 0         Lim::DEBUG and $self->{logger}->debug('Unable to reopen temp file');
633 0           return;
634             }
635            
636 0           $fh->seek(0, SEEK_END);
637 0           my $tell = $fh->tell;
638 0           $fh->seek(0, SEEK_SET);
639 0 0         unless ($fh->read($content, $tell) == $tell) {
640 0 0         Lim::DEBUG and $self->{logger}->debug('Unable to read temp file');
641 0           return;
642             }
643            
644 0 0         if ($sha eq Digest::SHA::sha1_base64($content)) {
645 0 0         Lim::DEBUG and $self->{logger}->debug('No change detected, checksum is the same');
646 0           return;
647             }
648            
649 0           return $content;
650             }
651              
652             =back
653              
654             =head1 AUTHOR
655              
656             Jerry Lundström, C<< >>
657              
658             =head1 BUGS
659              
660             Please report any bugs or feature requests to L.
661              
662             =head1 SUPPORT
663              
664             You can find documentation for this module with the perldoc command.
665              
666             perldoc Lim::CLI
667              
668             You can also look for information at:
669              
670             =over 4
671              
672             =item * Lim issue tracker (report bugs here)
673              
674             L
675              
676             =back
677              
678             =head1 ACKNOWLEDGEMENTS
679              
680             =head1 LICENSE AND COPYRIGHT
681              
682             Copyright 2012-2013 Jerry Lundström.
683              
684             This program is free software; you can redistribute it and/or modify it
685             under the terms of either: the GNU General Public License as published
686             by the Free Software Foundation; or the Artistic License.
687              
688             See http://dev.perl.org/licenses/ for more information.
689              
690              
691             =cut
692              
693             1; # End of Lim::CLI