Thursday 21 February 2013

Give your application a shell (and never write any ad-hoc scripts again)

As developers, we sometimes have to help operations going smoothly by fiddling with the data "by hand" because there's no GUI to allow people doing some rare and obscure things. One common way of doing it is by connecting to the DB and writing some SQL. But often, accessing your database is not enough, because you need your application code to be run.

And for that, you need a shell.


I mean a Perl application shell

Perl offers a great module to implement this: Devel::REPL . By the way, if your using a debian based distribution and if you're wondering about the quickest way to install a CPAN module, CPAN ↔ Debian is the site you want !

Let's say your application is called 'My::Bob'. In this example, it's fully implemented in the shell script itself, because well, it doesn't do much.

So here it is, the magic bob.pl shell for your My::Bob application:


use strict;
use warnings;
package My::Bob;
use Moose;
## Application bob just holds a name and a couple of methods.
has 'name' => ( is => 'rw', isa => 'Str' , default => 'Bob' );
sub hello{
 my ($self) = @_;
 return 'Hello, my name is '.$self->name();
}
sub count_to{
 my ($self, $limit) = @_;
 if( ( $limit // 0 ) < 1 ){ confess "limit should be >= 1"; }
 return map{ $_ } 1..$limit;
}

package main;
## The shell code starts here
use Devel::REPL;

## You probably want to skip output buffering
$| = 1;
# Build an instance of your application.
our $BOB = My::Bob->new();

print "Hello, this is bob\n";


my $repl = Devel::REPL->new;

## Inject the instance of bob in the shell lexical environment.
$repl->load_plugin('LexEnv');
$repl->lexical_environment->do(q|my $bob = $main::BOB ;|);

## Tell something useful in the prompt
$repl->load_plugin('FancyPrompt');
$repl->fancy_prompt(sub {
 my $self = shift;
 sprintf ('Bob (%s) > ',
 $BOB->name()
 );
});

## Various autocompletion.
$repl->load_plugin('CompletionDriver::LexEnv');
$repl->load_plugin('CompletionDriver::Methods');
$repl->load_plugin('CompletionDriver::INC');

## Allow multiline statements.
$repl->load_plugin('MultiLine::PPI');

# And run!
$repl->run();

print "\nThanks for using Bob. Bye\n";


Now, let's look at a session:


$ perl bob.pl 
Hello, this is bob
                                                                                                                                                        
Bob (Bob) > $bob->hello(); ## Call any method.
Hello, my name is Bob                                                                                                                                   Bob (Bob) > $bob->name('Bobbage'); ## Change of name is reflected in the prompt
Bobbage                                                                                                                                                 Bob (Bobbage) > $bob->count_to(3) ## Arrays are printed
1 2 3

Bob (Bobbage) > my $count_hello = sub{ my ($n) = @_;  ## Define multiline subs                                                                                                                                                       re.pl(main):005:1* map{ $bob->hello() } $bob->count_to($n);                                                                                                                            re.pl(main):006:1* }

CODE(0x2fcbea0)
Bob (Bobbage) > &$count_hello(3) ## And call them
Hello, my name is Bobbage Hello, my name is Bobbage Hello, my name is Bobbage

Bob (Bobbage) >  ## CTRL-D ends the session
Thanks for using Bob. 

Hope this short example was useful to you. If you can build your application as an object, writing your own shell should be straight forward. Also you might want to add some credential checking before you let anyone playing with the guts of your application.

Until next time, happy coding!

Jerome.



4 comments:

  1. Great example, thanks! I'm just curious about completion. What is it supposed to complete? Perl keywords such as 'print'? I don't get anything...

    Also, I read in Devel::REPL that the up arrow is supposed to climb up the history (as in the bash shell), but that doesn't work either for me. Some magic setup to apply maybe?

    ReplyDelete
  2. Hi Jean,
    Here I've got Devel::REPL version 1.003012. I've got history and autocompletion of variable names, method names and module names (not of perl native functions) in my shell.

    Maybe all of this depends on your binding of Term::Readline?

    I've got libterm-readline-perl-perl 1.0303-1 here. (On Ubuntu 12.04.2 LTS)

    ReplyDelete
  3. You're right. The culprit is Term::ReadLine. Everything is ok after I installed Term::ReadLine::Gnu.

    Thanks for the tip!

    ReplyDelete
    Replies
    1. Glad it works :) Sorry I'm not really aware of all the flavours of Term::ReadLine.

      Enjoy the new toy!

      Delete