readline
I’ve experimented a bit with various readline implementations in Perl. There’s quite a few on the CPAN, like you’d expect.
I’ve used Term::ReadLine::Gnu for some projects; most notably the shell functionality in pimpd2 and I thought it was pretty good. The only thing missing was a sane keymap (that is, a vi-mode).
So I went looking for modules that might support this. The first hit was Term::ReadLine::Zoid with its subclass ViCommand. This was spot on.
A nice thing with the readline implementations is that they’re all compatible; you don’t have to change the codebase if you switch module. So I had a snippet like this:
use strict;
use Term::ReadLine;
use B::Keywords qw(@Symbols);
use Eval::WithLexicals;
use Data::Dumper;
{
package Data::Dumper;
no strict 'vars';
$Terse = $Indent = $Useqq = $Deparse = $Sortkeys = 1;
$Quotekeys = 0;
}
my $term = Term::ReadLine->new('re.pl');
my $attr = $term->Attribs;
$attr->{completion_function} = sub {
my($word, $buffer, $start) = @_;
return (@Symbols);
};
$attr->{autolist} = 0;
$attr->{maxcomplete} = 0;
my $eval = Eval::WithLexicals->new;
while(1) {
my $line = $term->readline('> ');
my @ret;
eval {
local $SIG{INT} = sub { die "Caught SIGINT"; };
@ret = $eval->eval($line);
1;
} or @ret = ("Error!", $@);
print Dumper @ret;
}
This simple REPL tabcompletes to the imported symbols from the B::Keywords package, and it works like you’d expect with Term::ReadLine::Gnu. To try this out with Term::ReadLine::Zoid we execute it like so;
PERL__RL=Zoid perl re.pl
The vi-mode worked out of the box and I played with it for a bit. Then I noticed that using f, F, t, or T in normal mode didn’t work quite so well;
> while(1) { # pressing <ESC>, F, ( results in...
Unmatched ( in regex; marked by <-- HERE in m/.*((?:( <-- HERE / at ViCommand.pm line 544
Alright. Obvious what happens here; the ‘(‘ is being treated as a pattern metacharacter by the regex compiler. Should be an easy fix.
--- ViCommand.pm.orig 2004-11-22 14:09:23.000000000 +0100
+++ ViCommand.pm.fix 2011-01-20 17:57:11.283335132 +0100
@@ -537,12 +537,14 @@
my ($l, $x) = ( $$self{lines}[ $$self{pos}[1] ], $$self{pos}[0] );
if ($key eq 'T' or $key eq 'F') {
$l = substr($l, 0, $x);
- return $self->bell unless $l =~ /.*((?:$chr.*){$cnt})$/;
+ # We do not want $chr to be interpreted as pattern metacharacters
+ # Avoid 'unmatched "(" in regex'
+ return $self->bell unless $l =~ /.*((?:\Q$chr\E.*){$cnt})$/;
$$self{pos}[0] -= length($1) - (($key eq 'T') ? 1 : 0);
return length($1);
}
else { # ($key eq 't' || $key eq 'f')
- return $self->bell unless $l =~ /^..{$x}((?:.*?$chr){$cnt})/;
+ return $self->bell unless $l =~ /^..{$x}((?:.*?\Q$chr\E){$cnt})/;
$$self{pos}[0] += length($1) - (($key eq 't') ? 1 : 0);
return length($1);
}
I’ve sent the patch upstream; the module hasn’t been updated since 2004 though, so just in case a fork can be found here, and the diff here. After applying that fix, the vi-mode worked just fine. So time to find another obstacle…
Term::ReadLine::Zoid does not come with a completion function by default. The completion function I defined in the above code snippet simply returned all strings in the @Symbols array every time TAB was pressed. Not what I want. So this means I’ll have to create a new completion function from scratch and inject it into Zoid. Or does it?
No.
I started reading the documentation one more time. I had missed a very important thing in the Zoid documentation:
It features almost all key-bindings described in the posix spec for the sh(1) utility with some extensions like multiline editing; this includes a vi-command mode with a save-buffer (for copy-pasting) and an undo-stack.
I couldn’t see why the Gnu implementation wouldn’t support this. So I turned to the mighty interweb and found a reference to RL_STATE_VIMOTION. This should mean there is support for vi-mode, even though not explicitly stated in the documentation… The ‘Selecting a Keymap’ section caught my eye.
"make_bare_keymap"
Keymap rl_make_bare_keymap()
"copy_keymap(MAP)"
Keymap rl_copy_keymap(Keymap|str map)
"make_keymap"
Keymap rl_make_keymap()
"discard_keymap(MAP)"
Keymap rl_discard_keymap(Keymap|str map)
"free_keymap(MAP)"
void rl_free_keymap(Keymap|str map)
"get_keymap"
Keymap rl_get_keymap()
"set_keymap(MAP)"
Keymap rl_set_keymap(Keymap|str map)
"get_keymap_by_name(NAME)"
Keymap rl_get_keymap_by_name(str name)
"get_keymap_name(MAP)"
str rl_get_keymap_name(Keymap map)
Not that much of a description, but let’s play around a bit…
# bless( do{\(my $o = "-1221627264")}, 'Keymap' )
print Dumper $term->get_keymap();
# bless( do{\(my $o = "-1220973856")}, 'Keymap' )
print Dumper $term->get_keymap_by_name('vi');
# Whoah.
$term->set_keymap('vi');
And there we have it; a nice, working tabcompletion and a sane keymap. Time to write a sane REPL, using the module I first started out with 2 hours ago…