Lim

 view release on metacpan or  search on metacpan

lib/Lim/CLI.pm  view on Meta::CPAN


our $VERSION = $Lim::VERSION;
our @BUILTINS = (qw(quit exit help));

=head1 SYNOPSIS

=over 4

use Lim::CLI;

$cli = Lim::CLI->new(...);

=back

=head1 DESCRIPTION

This is the CLI that takes the input from the user and sends it to the plugin in
question. It uses L<AnyEvent::ReadLine::Gnu> if it is available and that enables
command line completion and history functions. It will load all plugins present
on the system and use their CLI part if it exists.

Failing to have a supported readline module it will use a basic
L<AnyEvent::Handle> to read each line of input and process it.

Built in commands that can not be used by any plugins are:

=over 4

quit - Will quit the CLI
exit - Will exit the relative section or quit the CLI
help - Will show help for the relative section where the user is

=back

=head1 METHODS

=over 4

=item $cli = Lim::CLI->new(key => value...)

Create a new Lim::CLI object.

=over 4

=item on_quit => $callback->($cli_object)

Callback to call when the CLI quits, either with the user doing CTRL-D, CTRL-C
or the command 'quit'.

=back

=cut

sub new {
    my $this = shift;
    my $class = ref($this) || $this;
    my %args = ( @_ );
    my $self = {
        logger => Log::Log4perl->get_logger($class),
        cli => {},
        busy => 0,
        no_completion => 0,
        prompt => 'lim> '
    };
    bless $self, $class;
    weaken($self->{logger});
    my $real_self = $self;
    weaken($self);

    unless (defined $args{on_quit}) {
        confess __PACKAGE__, ': Missing on_quit';
    }
    unless (ref($args{on_quit}) eq 'CODE') {
        confess __PACKAGE__, ': on_quit is not CODE';
    }
    $self->{on_quit} = $args{on_quit};

    foreach my $module (qw(Lim::Agent)) {
        my $name = lc($module->Name);

        if (exists $self->{cli}->{$name}) {
            Lim::WARN and $self->{logger}->warn('Can not load internal CLI module ', $module, ': name ', $name, ' already in use');
            next;
        }

        if (defined (my $obj = $module->CLI(cli => $self))) {
            $self->{cli}->{$name} = {
                name => $name,
                module => $module,
                obj => $obj
            };
        }
    }

    foreach my $module (Lim::Plugins->instance->LoadedModules) {
        my $name = lc($module->Name);

        if (exists $self->{cli}->{$name}) {
            Lim::WARN and $self->{logger}->warn('Can not use CLI module ', $module, ': name ', $name, ' already in use');
            next;
        }

        if (defined (my $obj = $module->CLI(cli => $self))) {
            $self->{cli}->{$name} = {
                name => $name,
                module => $module,
                obj => $obj
            };
        }
    }

    eval {
        require AnyEvent::ReadLine::Gnu;
    };
    unless ($@) {
        $self->{rl} = AnyEvent::ReadLine::Gnu->new(
            prompt => 'lim> ',
            on_line => sub {
                unless (defined $self) {
                    return;
                }

lib/Lim/CLI.pm  view on Meta::CPAN

            unless (defined $self) {
                return;
            }
            $self->{on_quit}($self);
        },
        on_read => sub {
            my ($handle) = @_;

            $handle->push_read(line => sub {
                shift;
                unless (defined $self) {
                    return;
                }
                $self->process(@_);
            });
        });

        IO::Handle::autoflush STDOUT 1;
    }

    if (defined (my $appender = Log::Log4perl->appender_by_name('LimCLI'))) {
        Log::Log4perl->eradicate_appender('Screen');
        $appender->{cli} = $self;
        weaken($appender->{cli});
    }

    $self->println('Welcome to LIM ', $Lim::VERSION, ' command line interface');
    $self->prompt;

    Lim::OBJ_DEBUG and $self->{logger}->debug('new ', __PACKAGE__, ' ', $self);
    $real_self;
}

sub DESTROY {
    my ($self) = @_;
    Lim::OBJ_DEBUG and $self->{logger}->debug('destroy ', __PACKAGE__, ' ', $self);

    if (exists $self->{rl}) {
        if (Lim::Config->{cli}->{history_file}) {
            $self->{rl}->WriteHistory(Lim::Config->{cli}->{history_file});
        }
    }

    delete $self->{current};
    delete $self->{rl};
    delete $self->{stdin_watcher};
    delete $self->{cli};
}

=item $cli->process($line)

Process a line of input, called from the input watcher
(L<AnyEvent::ReadLine::Gnu> or L<AnyEvent::Handle>).

=cut

sub process {
    my ($self, $line) = @_;
    my ($cmd, $args);

    if ($self->{busy}) {
        return;
    }

    if (defined $line) {
        ($cmd, $args) = split(/\s+/o, $line, 2);
        $cmd = lc($cmd);
    }
    else {
        $cmd = 'quit';
    }

    if ($cmd eq 'quit') {
        $self->{on_quit}($self);
        return;
    }
    elsif ($cmd eq 'exit') {
        if (exists $self->{current}) {
            delete $self->{current};
            $self->set_prompt('lim> ');
            $self->prompt;
        }
        else {
            $self->{on_quit}($self);
            return;
        }
    }
    elsif ($cmd eq 'help') {
        if (exists $self->{current}) {
            $self->print_command_help($self->{current}->{module}->Commands);
        }
        else {
            my @cmds = keys %{$self->{cli}};
            push(@cmds, @BUILTINS);
            $self->println('Available commands: ', join(' ', sort @cmds));
        }
        $self->prompt;
    }
    else {
        if ($cmd) {
            if (exists $self->{current}) {
                if ($self->{current}->{module}->Commands->{$cmd} and
                    $self->{current}->{obj}->can($cmd))
                {
                    $self->{busy} = 1;
                    $self->set_prompt('');
                    $self->{current}->{obj}->$cmd($args);
                }
                else {
                    $self->unknown_command($cmd);
                }
            }
            elsif (exists $self->{cli}->{$cmd}) {
                if ($args) {
                    my $current = $self->{cli}->{$cmd};
                    ($cmd, $args) = split(/\s+/o, $args, 2);
                    $cmd = lc($cmd);

                    if ($current->{module}->Commands->{$cmd} and
                        $current->{obj}->can($cmd))
                    {
                        $self->{busy} = 1;
                        $self->set_prompt('');
                        $current->{obj}->$cmd($args);
                    }
                    else {
                        $self->unknown_command($cmd);
                    }
                }
                else {
                    $self->{current} = $self->{cli}->{$cmd};
                    $self->set_prompt('lim'.$self->{current}->{obj}->Prompt.'> ');
                    $self->prompt;
                }
            }
            else {
                $self->unknown_command($cmd);
            }
        }
        else {
            $self->prompt;
        }
    }
}

=item $cli->prompt

Print the prompt, called from C<process>.

=cut

sub prompt {
    my ($self) = @_;

    if (exists $self->{rl}) {
        return;
    }

    $self->print($self->{prompt});
    IO::Handle::flush STDOUT;
}

=item $cli->set_prompt

Set the prompt, called from C<process>.

=cut

sub set_prompt {
    my ($self, $prompt) = @_;

    $self->{prompt} = $prompt;

    if (exists $self->{rl}) {
        $self->{rl}->hide;
        $AnyEvent::ReadLine::Gnu::prompt = $prompt;
        $self->{rl}->show;
    }

    $self;
}

lib/Lim/CLI.pm  view on Meta::CPAN

        $self->{rl}->print(@_, "\n");
        $self->{rl}->show;
    }
    else {
        foreach (@_) {
            print;
            IO::Handle::flush STDOUT;
        }
        print "\n";
        IO::Handle::flush STDOUT;
    }

    $self;
}

=item $cli->print_command_help($module->Commands)

Print the help for all commands from a plugin.

=cut

sub print_command_help {
    my ($self, $commands, $level) = @_;
    my $space = ' ' x ($level * 4);

    if (ref($commands) eq 'HASH') {
        foreach my $key (sort (keys %$commands)) {
            if (ref($commands->{$key}) eq 'HASH') {
                $self->println($space, $key);
                $self->print_command_help($commands->{$key}, $level+1);
            }
            elsif (ref($commands->{$key}) eq 'ARRAY') {
                if (@{$commands->{$key}} == 1) {
                    $self->println($space, $key, ' - ', $commands->{$key}->[0]);
                }
                elsif (@{$commands->{$key}} == 2) {
                    $self->println($space, $key, ' ', $commands->{$key}->[0], ' - ', $commands->{$key}->[1]);
                }
                else {
                    $self->println($space, $key, ' - unknown/invalid help');
                }
            }
            else {
                $self->println($space, $key, ' - no help');
            }
        }
    }

    $self;
}

=item $cli->Successful

Called from L<Lim::Component::CLI> when a command was successful.

=cut

sub Successful {
    my ($self) = @_;

    $self->{busy} = 0;
    if (exists $self->{current}) {
        $self->set_prompt('lim'.$self->{current}->{obj}->Prompt.'> ');
    }
    else {
        $self->set_prompt('lim> ');
    }
    $self->prompt;
    return;
}

=item $cli->Error($LimError || @error_text)

Called from L<Lim::Component::CLI> when a command issued an error. The error can
be a L<Lim::Error> object or list of strings that will be joined to produce an
error string.

=cut

sub Error {
    my $self = shift;

    $self->print('Command Error: ', ( scalar @_ > 0 ? '' : 'unknown' ));
    foreach (@_) {
        if (blessed $_ and $_->isa('Lim::Error')) {
            $self->print($_->toString);
        }
        else {
            $self->print($_);
        }
    }
    $self->println;

    $self->{busy} = 0;
    if (exists $self->{current}) {
        $self->set_prompt('lim'.$self->{current}->{obj}->Prompt.'> ');
    }
    else {
        $self->set_prompt('lim> ');
    }
    $self->prompt;
}

=item $cli->Editor($content)

Call up an editor for the C<$content> provided. Will return the new content if
it has changed or undef on error or if nothing was changed.

Will use L<Lim::Config>->{cli}->{editor} which will be the environment variable
EDITOR or what ever your configure it to be.

=cut

sub Editor {
    my ($self, $content) = @_;
    my $tmp = File::Temp->new;
    my $sha = Digest::SHA::sha1_base64($content);

    Lim::DEBUG and $self->{logger}->debug('Editing ', $tmp->filename, ', hash before ', $sha);

    print $tmp $content;
    $tmp->flush;

    # TODO check if editor exists

    if (system(Lim::Config->{cli}->{editor}, $tmp->filename)) {
        Lim::DEBUG and $self->{logger}->debug('EDITOR returned failure');
        return;
    }

    my $fh = IO::File->new;
    unless ($fh->open($tmp->filename)) {
        Lim::DEBUG and $self->{logger}->debug('Unable to reopen temp file');
        return;
    }

    $fh->seek(0, SEEK_END);
    my $tell = $fh->tell;
    $fh->seek(0, SEEK_SET);
    unless ($fh->read($content, $tell) == $tell) {
        Lim::DEBUG and $self->{logger}->debug('Unable to read temp file');
        return;
    }

    if ($sha eq Digest::SHA::sha1_base64($content)) {
        Lim::DEBUG and $self->{logger}->debug('No change detected, checksum is the same');
        return;
    }

    return $content;
}

=back



( run in 2.319 seconds using v1.01-cache-2.11-cpan-5837b0d9d2c )