Wednesday, December 29, 2010

Perl command line emulator (an exercise in using remote API calls for command completion/execution)

OK--that's an awkward title. The real deal is this...

This was an exercise in seeing if a command line interface could be built using a remote source (meaning accessing an REST API via http) for command completion and execution. The remote API used was the Vyatta router remote access API (REST based https configuration API). I choose PERL as the language so that this little prototype could run on any system (windows, mac, linux etc.). Sure the language could have been something else, but what the heck I picked Perl--plus it has pretty good JSON support (which is the response body format for the remote service).



Commands are dispatched to a remote server via curl with the REST API url and http command. The main functionality supported by these commands include command completion and execution. The cool thing about all this is that it actually does works--meaning that the processing of each character (in command completion mode for example) is fast enough to support a remote procedure call and the overhead of processing associated with command line matching. And when the command structure and execution can be located remotely, is fast enough, and accessed via any language then this means that a CLI supported by a distributed API with reasonable performance can exist nearly anywhere (i.e. in browsers etc.). Neat.

In this case the linchpin (for me) was getting a command line character processor up and running--all else was a smop (or just gluing together the remaining https commands, JSON and string foo). Turns out that the ReadKey perl module provides single character blocking reads. And does a pretty good job at it. The code below is responsible for reading, interpreting and then printing the results character by character. And with this I ended up adding support for support character deletion, backspacing and history buffer as well. As mentioned, the processing between characters (keystrokes) needs to be suitably fast to feel responsive enough.

#!/usr/bin/perl                                                                                          
use strict;
use warnings;
use lib "/opt/vyatta/share/perl5/";
use Term::ReadKey;

my $prompt = "my-cli";

open(TTY, " ");

while () {
    my $last_char = '';
    my $input = '';
    my $history_index = 0;
    while (1) {
        ReadMode("raw");
        my $in = ReadKey(0, *TTY);
        ReadMode("normal");
#       printf "\nYou said %s, char number %03d\n",$in, ord $in;                                         
        my $charkey = ord $in;
        if ($charkey ==  9) { #TAB                                                                       
           #Execute a remote call to command match here
           #process the response
           #fill out remainer of command or options
        }
        if ($charkey == 127 || $charkey == 8) { #BACKSPACE                                               
        }
        elsif ($charkey == 24) { #CTRL-X                                                                 
            exit(0);
        }
        elsif ($charkey == 13) { #Carriage-Return                                                                 
            #execute command!
            #then clear buffer
        }
        else {
            print $in;
        }
    }
}

So, in the snippet above there are a couple things going on here:

1) opening the system tty for reading
2) setting read mode to "raw", which means that the tty provides no interpretation of the typed in character (but is different from "ultra-raw" which disables conversion of LF to CR/LF conversion--but given the different targets we should probably stick with "raw" mode).
3) Finally, the call to ReadKey which performs a blocked read, and returns control once a character is typed. This call unblocks on each typed in character. The buffer is maintained within the while loop until a command is matched (the rest of the command is filled out), or a carriage return is typed (execute the command), or other various control characters (ctrl-x, delete, backspace, etc.).

What's great here is that as each character is typed I can perform a http fetch to a remote API and compare the typed in command against the response via the remote http server and command complete.

Something like:

my-cli> s
remote API fetch returns "set"
my-cli> set[TAB]
remote API fetch returns command completions for "monkey" and "banana"
my-cli> set {monkey banana}
my-cli> set m[TAB]
remote API fetch returns command completions for "monkey" and "banana"
command completes to "monkey":
my-cli> set monkey [cr]
remote API executes command "set monkey"
A successful emulation in this case is one that provides a fast enough response via the remote http fetch, and a command line feature set that is similar to a "real" CLI, mainly that provides command line history and an editable command line. To support the former an array of completed commands is all that is required, and the latter requires supporting backspace/delete.

Obviously, at some point network speed becomes the limiting factor--and processing speed doesn't come into play at the point that latency on the network is the largest portion of the delay. One mitigating factor that can be applied is to read ahead and pre-fetch available options in the background on all possible matches.

Send me a PM for the complete source (I left it out since it's a ton of string manipulation specific to this example).

Not exactly rocket-science, but very nice to see this work...

No comments:

Post a Comment