shells-post-icon

Readline is something everyone takes for granted, and it makes interacting with a program much more enjoyable than simply typing at a program that reads directly from STDIN. It gives you options for tab complete, line editing and user configurability that you would have to implement yourself otherwise.

If you've got a complex AnyEvent program running, with loads of stuff going on, an interactive shell can make your program more enjoyable for both you and your users.

Since shells have already taught us how background tasks work, we can provide a comfortable interface for users familiar with job control in bash.

You can get all this lovely functionality from AnyEvent::ReadLine::Gnu, just throw it in your event loop and you're good to go.

A simple shell with AnyEvent::ReadLine::Gnu

As a start you can drop this stuff in a file (that I've called shell-only):

package Computer::Program;
use warnings; use strict;

# Set up some rw accessors.
use Object::Tiny::RW 1.07 (
    shell       => # Our readline object 
    cv          => # Our condvar, which serves as our mainloop
    delay_timer => # The condvar for our background task. 
);

sub run {
    my $self = shift;
    use AnyEvent 7.11;
    $self->cv( AnyEvent->condvar );

    # Users can type at this thing.
    use AnyEvent::ReadLine::Gnu 1.0;
    $self->shell( AnyEvent::ReadLine::Gnu->new(
        prompt => 'How can I help you? > ',
        on_line => sub {
            my ($line) = @_;
            $self->handle_command( $line )
        },
    ));

    $self->cv->recv; # run the loop. Real programs will C< EV::loop > or similar.
}

# these are the actual commands, in a "dispatch table":
my %commands; %commands = (
    'help' => sub {
        my ($self) = @_;
        $self->shell->print(        # this always needs \n
            sprintf "Try one of '%s'\n", join ', ', sort keys %commands
        );
    },
    'wait' => sub {
        my ($self, $delay) = @_;

        # If one is scheduled, let it run:
        if ($self->delay_timer()) {
            $self->shell->print( "You're already waiting. Please wait harder..\n");
            return;
        }

        # Store the timer guard away
        # (if you don't store the guard, it goes out of scope, and cancels the timer)
        $self->delay_timer(
            AnyEvent->timer(after => $delay, cb => sub {
                my $s=$delay == 1 ? '' : 's' ; # English.
                $self->shell->print( "It has been $delay second$s, sir.\n");
                $self->delay_timer(undef);
            })
        );
    },
    'exit' => sub { 
        my $self = shift;
        $self->cv->send 
    },
);

# parse text from the user with the magical C< split '' >
sub handle_command {
    my $self = shift;
    my ($requested, @args) = split ' ', shift;
    # check defaults, use help if $command is bogus
    if (defined $requested and exists $commands{ $requested }) {
        $commands{ $requested }->( $self, @args )
    }
    else {
        $commands{ help }->( $self )
    }
}

# kick off the program, but only when run as perl $filename
__PACKAGE__->new->run() unless caller;

A Quick note on dependency management

Since this is just a quick program for a blog post, and not a real project we'll just install the modules we need manually instead of packaging it properly:

 cpanm AnyEvent@7.11 Object::Tiny::RW@1.07 AnyEvent::ReadLine::Gnu@1.0

If you don't have cpanm you can bootstrap it from cpanmin.us with:

% curl -L https://cpanmin.us | perl - App::cpanminus

There isn't anything magical about these versions, they are just the latest ones at the time of writing.

Let's run this puppy:

It doesn't really do anything interesting at all, but let's fire it up:

me@compy386:~ perl shell-only 

It will politely prompt us for instructions:

How can I help you? > help
Try one of 'exit, help, wait'
How can I help you? > wait 1

After about a second passes:

It has been 1 second, sir.
How can I help you? > wait 10
How can I help you? > wait 1
You're already waiting. Please wait harder..

and then, roughly 9 seconds later:

It has been 10 seconds, sir.
How can I help you? > 

We can see that the timers keep running while the prompt is waiting for input, so we know that our event loop is turning in the background. That would be amazing news if it was doing something useful.

How can I help you? > exit
me@compy386:~ 

TL;DR: AnyEvent::ReadLine::Gnu is just fine

There are some notes in the docs suggesting that readline might block and stall your event loop, leading to your program freezing or deadlocking.

Those things are easy enough to deal with but they mostly end up with you doing your work in another process/thread.

Exercises for the reader

1. Multiple timers

If you're feeling frisky you could patch this script to support multiple concurrent timers.

How can I help you? > wait 10
How can I help you? > wait 15

9 or so seconds pass:

The first timer fired!
How can I help you? >

5 more seconds pass:

The second timer fired!
How can I help you? >

2. Implement task management via a jobs commnad

It will take extra book keeping to keep track of the timers that are running, when they started and when they're due to complete. Carefully stashing background information a background_tasks attribute.

How can I help you? > jobs
    [1] wait 5     -  1 seconds left
    [2] wait 15    - 11 seconds left

Depending on the background task you could even ->send on the cv to stop the task.

3. Actually doing useful things in the background

The shell could trigger multiple jobs of some type, even to run a bunch of things at the same time with AnyEvent::HTTP or AnyEvent::Run. You could even control a farm with AnyEvent::MP.