Alex
Alex

Reputation: 6037

dynamic cmd.Cmd tab-completion

I try to use Cmd , but instead of adding all functions/commands separately, I want it to read functionnames from a dictionary and add them dynamically. The preloop should add functions do_aa, complete_aa, do_bb and complete_bb. The commands are taken from a dictionary, and added using setattr

import cmd

class C(cmd.Cmd):

    commands = {'aa': ['aa1', 'aa2'],
                'bb': ['bb1', 'bb2']}

    def preloop(self):
        for k, v in self.commands.items():
            self.k = k
            setattr(C, 'do_' + k, k)
            setattr(C, 'complete_' + k, self.complete_)

    def complete_(self, text, line, start_index, end_index):
        if text:
            return [
                command for command in self.commands[self.k]
                if command.startswith(text)
            ]
        else:
            return self.commands[self.k]


if __name__ == '__main__':
    C().cmdloop()

The problem is that the autocomplete does not actually get the correct second-level commands:

(Cmd) aa 
bb1  bb2  # should be aa1 aa2
(Cmd) bb
bb1  bb2 

Why is this happening, and how can this be solved?

Upvotes: 1

Views: 241

Answers (1)

Marco Luzzara
Marco Luzzara

Reputation: 6026

Your script does not work with Python 3.8.10 probably because of this line

setattr(C, 'do_' + k, k)

k is not a method. By the way, the mistake is in this line:

for k, v in self.commands.items():
     self.k = k  # <--

You are continuously reassigning self.k until the last key of the commands dictionary, that is bb. Later, the complete_ method is always evaluated as self.commands['bb']. Here is a possible working version:

import cmd

class C(cmd.Cmd):

    commands = {
            'aa': ['aa1', 'aa2'],
            'bb': ['bb1', 'bb2']
        }
        
    def monad_print(self, *args):
        print(args)

    def preloop(self):
        for k, v in self.commands.items():
            setattr(C, 'do_' + k, lambda self, arg, k=k: self.monad_print(k, arg))
            setattr(C, 'complete_' + k, self.complete_)

    def complete_(self, text, line, start_index, end_index):
        command = line.split(' ', maxsplit=1)[0]
        if text:
            return [
                command for command in self.commands[command]
                if command.startswith(text)
            ]
        else:
            return self.commands[command]


if __name__ == '__main__':
    C().cmdloop()

The only difference is that the command is now taken from the entire read-line, in particular the first token of str.split().

Upvotes: 3

Related Questions