python’s cmd module is great. You derive your class from the provided command interpreter and you can start right away implementing command. Let’s have a look at a small command interpreter for a beer-drinking bot.
import cmd class BeerRobotCmd(cmd.Cmd): def __init__(self): cmd.Cmd.__init__(self) #for the sake of simplicity we make #those states part of the cmd object. Normally, #of course you would use your cmd class to drive #some external interface. self.stomach_filled = 0 self.drunkness = 0 self.singing = None def do_drink(self, line): if self.stomach_filled >= 100: print "robot is full of beer" else: print "robot drinks beer" self.stomach_filled += 10 self.drunkness += 1 def do_throwup(self, line): print "robot throws up" self.stomach_filled = 0 def do_sing(self, line): if len(line.strip()) > 0: if self.singing != None: print "robot is singing already" elif self.drunkness < 15: print "robot is too shy" else: print "robot starts to sing", line self.singing = line else: if self.singing: print "robot stops singing" commandline = BeerRobotCmd() commandline.cmdloop()
Our robot now understands 3 important commands
- drink makes him drink a beer, until he’s full.
- throwup makes him throw up (wow, surprising 🙂 ) so his stomach is empty again
- sing makes him sing a custom sing. You can tell him to sing, for example, the national anthem or a lullaby. If you call „sing“ without any arguments, he will stop to sing
We have amazingly created those commands simply by implementing 3 methods
def do_commandname(self, line)
In those line contains all the characters entered by the user after the actual command string. So in order to extract arguments from there, we simply have so split or search or process the line argument in some way.
This quickly-implemented command interpreter even contains completion for commands already. Pressing TAB without entering any character will show the user all avaible commands.
The cmd module allows you to implement a simple completion callback intended not for the command strings but for the arguments.
Here’s a completion callback for the sing command, proposing to the user a list of songs.
def complete_sing(self, text, line, startidx, endidx): return [ song for song in ['yesterday', 'uptown girl', 'anything goes', 'old mcdonald has a farm'] if song.startswith(text) ]
Like in the unix shell, where hitting TAB while entering a file name will prompt you with possible completions, this callback will provide possible completions as well. It works as follows
- the line argument contains the complete line, like in the do_foo methods
- the text argument contains the part of the current argument, the user has entered already. This is, of course, inteded to be used as a filter for the set of possible completions. If i enter „sing yest“ and hit TAB, this callback is called with text = „yest“.
- the startidx and endidx arguments are indices into the line string, describing which subset of line is contained in text. For the case of „sing yest“ and hitting TAB, those args would contains 1 and 4 respectively (not 0, because the space between „sing“ and „yest“ is interpreted as the first character of the line string).
argument splitting problem
Well, this is all great. Until you try to complete arguments containing characters, interpreted by the completion mechanism as separators. Like a guiltless „/“. This is bad. You won’t be able to implement the completion of file name arguments. Assume, for example, the user has entered „SOMECOMMAND /home/foo/bar/“ and wants to see now (similiar to the unix shell) what files are there in the directory /home/foo/bar/. He hit’s TAB. The completion mechanism scans the current command line and splits it into SOMECOMMAND<start of argument line> /<first argument>home</first argument>/<second argument>foo</second argument>/<third argument>bar</third argument>/<fourth argument></fourth argument>. Now your completion callback is called with text=““ (fourth argument). This does not work!
Maybe there’s a simple solution by changing the separators used? I didn’t find a way to do that. So I implemented the following workaround. First let’s introduce a function to do completion from filenames
import os def get_possible_filename_completions(text): head, tail = os.path.split(text.strip()) if head == "": #no head head = "." files = os.listdir(head) return [ f for f in files if f.startswith(tail) ] def extract_full_argument(line, endidx): newstart = line.rfind(" ", 0, endidx) return line[newstart:endidx]
The first function extract possible file names from a given input path. If you have files a, b and c in a folder /foo/bar/, passing „/foo/bar/“ to this function will result in the list [„a“, „b“, „c“]. Careful: In order to work together with the cmd completion mechanism splitting arguments at „/“ characters, this function must not return absolute file paths. Assume for a moment we would have written in the above code
return [ head + "/" + f for f in files if f.startswith(tail) ]
What would happen?
We would enter „SOMECOMMAND /foo/bar/a“, then hit TAB. get_possible_filename_completions would receive text=“/foo/bar/a“, split it into head=“/foo/bar“ and tail = „a“. It would then search in „/foo/bar“ and find [„a“, „b“, „c“]. This list would then be filtered and only „a“ would remain. Now, head + „/“ + f would be the full path „/foo/bar/a“. Now comes the important point: the cmd completion mechanism would substitute what it deems to be the argument to be completed, which would be „a“ for the return value of get_possible_filename_completions. So we would end up with „SOMECOMMAND /foo/bar//foo/bar/a“. This is certainly not what we inteded!
The second function is the workaround. It takes the end index for the argument – this is where the cursor is. It then searches from this point on for a space character left of the end index. This way it does not stop at the next „/“ character. It returns the complete argument, which can then be fed to get_possible_filename_completions.