#!/usr/bin/python
"""
Shellac
=======
shellac is an alternative to the standard python library `cmd <http://docs.python.org/2/library/cmd.html>`_ which aims to offer an alternative approach to nesting commands.
"""
import sys
import rl
import rl.readline as readline
import inspect
from functools import wraps
[docs]def completer(func):
"""Attach a completion function to the decorated function."""
def inner_completer(obj):
"""The inner decorator which takes the completion function as its only
argument."""
if not hasattr(obj, "completions"):
obj.completions = []
obj.completions.append(func)
return obj
return inner_completer
[docs]def members(obj, prefix='do_'):
"""Return a list of members of the given class which start with a given
prefix.
:type obj: class
:param obj: Class to inspoect for members of a given prefix.
:type prefix: string
:param prefix: The prefix which members of the given class must start with.
:return: list
"""
return (f[0][len(prefix):] for f in inspect.getmembers(obj) if f[0].startswith(prefix))
[docs]def complete_list(names, token, append_character=" "):
"""Filter given list which starts with the given string.
:type names: list
:param names: list to filter
:type token: string
:param token: 'startswith' filter token
:type append_character: string
:param append_character: completion character to append (see rl.completion.append_character)
:return: generator
"""
rl.completion.append_character = append_character
return (x for x in names if x.startswith(token))
[docs]class Shellac(object):
"""An interactive command interpreter.
You should never call this class directly. To use it, inherit from this
class and implement do_*() methods which map to * commands. Implement
child methods of classes defined in your subclass to create subcommands
in the interface.
:type completekey: *readline* name of a comlpetion key.
:param completekey: Key to execute completion
:type stdin: File-like object
:param stdin: Override stdin (defaults to *sys.stdin*)
:type stdout: File-like object
:param stdout: Override stdout (defaults to *sys.stdout*)
"""
def __init__(self, completekey='tab', stdin=sys.stdin, stdout=sys.stdout):
"""Create a command interpreter."""
self.stdin = stdin
self.stdout = stdout
self.completekey = completekey
if self.stdin.isatty():
self.prompt = "(%s) " % (self.__class__.__name__)
else:
self.prompt = ""
self.lastcmd = ''
self.intro = None
self.cmdqueue = []
# raw_input() replaced with input() in python 3
try:
self.inp = raw_input
except NameError:
self.inp = input
[docs] def emptyline(self):
"""Method to specify what happens when an empty line is entered.
*Can be overridden*.
"""
return
[docs] def default(self, line):
"""Default action for commands with no do_ method.
*Can be overridden*.
"""
self.stdout.write('*** Unknown syntax: {0}\n'.format(line))
[docs] def do_exit(self, args):
"""Exit the interactive interpreter."""
return True
do_EOF = do_exit
[docs] def do_help(self, args):
"""Help on help"""
self.stdout.write((self._get_help(args, self) or
"*** No help for %s" % (args or repr(self))) + "\n")
@classmethod
def _get_help(cls, args, root):
"""Recursive class method to find a help string for the given command.
Returns either a string from the result of a help_*() or do_*()
function, the do_*() function's docstring or None.
"""
try:
cmd, args = args.split(None, 1)
except ValueError:
cmd = args
args = ''
if not cmd:
return root.__doc__
if inspect.isclass(root):
root = root()
try:
func = getattr(root, 'help_' + cmd)
except AttributeError:
if hasattr(root, 'do_' + cmd):
return cls._get_help(args, getattr(root, 'do_' + cmd)) or \
getattr(root, 'do_' + cmd).__doc__
else:
return func(args)
[docs] def precmd(self, line):
"""Hook method executed just before the command line is dispatched.
*Can be overridden*.
"""
return line
[docs] def postcmd(self, stop, line):
"""Hook method executed just after a command dispatch is finished.
*Can be overridden*.
:type stop: None or True
:param stop: flag passed in from onecmd() which is usually returned
:type line: string
:param line: line executed by onecmd
:return: Return True (stop) to cause oneloop() to break
"""
return stop
[docs] def preloop(self):
"""Hook method executed once when the cmdloop() method is called.
*Can be overridden*.
"""
pass
[docs] def postloop(self):
"""Hook method executed once when the cmdloop() method is finished.
*Can be overridden*
"""
pass
[docs] def ctrl_c(self, exc):
"""Hook method called when Ctrl-C is pressed during execution of loop body.
*Can be overridden*.
"""
pass
[docs] def cmdloop(self):
"""Implement an interactive command interpreter which grabs a line of
input and passes it to onecmd() until the postcmd() function returns
True.
This method will also:
* Execute a preloop() method before starting the interpreter
* Install a complete() readline completer function
* Write the string intro followed by a newline to stdout
* Read from a list of commands called cmdqueue, or
* Read from stdin, and
* Call precmd() with the line as an argument,
* Call onecmd() with the line as an argument,
* Call postcmd() with the stop flag and the line as an argument.
* Finally, restore the previous readline completer, if any.
"""
self.preloop()
old_completer = readline.get_completer()
readline.set_completer(self.complete)
readline.parse_and_bind(self.completekey + ": complete")
try:
if self.intro:
self.stdout.write(str(self.intro) + "\n")
stop = None
while not stop:
if self.cmdqueue:
line = self.cmdqueue.pop()
else:
try:
line = self.inp(self.prompt)
except EOFError:
self.stdout.write("\n")
line = 'EOF'
except KeyboardInterrupt as exc:
self.ctrl_c(exc)
self.cancel()
continue
try:
line = self.precmd(line)
stop = self.onecmd(line)
stop = self.postcmd(stop, line)
except KeyboardInterrupt as exc:
self.ctrl_c(exc)
self.cancel()
self.postloop()
finally:
readline.set_completer(old_completer)
[docs] def onecmd(self, line, args='', root=None):
"""Execute a single command line.
If the given line is False (i.e. empty), call return the result of
emptyline(). Thereafter, try to find a chain of do_*() methods and
classes which ends with a callable, then return the result of calling
it.
:type line: string
:param line: line to be executed
:type args: string
:param args: used to store 'current' part of line during recursion
:type root: object
:param root: 'current' 'do_' class or method during recursion
"""
if not args:
args = line
if not root:
root = self
if args:
try:
child, args = args.split(None, 1)
except ValueError:
child = args
args = ''
elif not line:
return self.emptyline()
self.lastcmd = line
if line == 'EOF': # http://bugs.python.org/issue13500
self.lastcmd = ''
try:
root = getattr(root, 'do_' + child)
except AttributeError:
return self.default(line)
if inspect.isclass(root):
# If a class, we must instantiate it
root = root()
try:
# Is root (really) callable
return root(args)
# python2 and 3 return different exceptions here
except (AttributeError, TypeError):
# It wasn't callable, recurse
if not args:
return self.default(line)
return self.onecmd(line, args, root)
# traverse_help is recursive so needs to find itself through the class
@classmethod
def _traverse_help(cls, tokens, tree):
"""Recurse through the class tree of do_*() methods and classes to find
a help_*() method which can be used to provide help.
:type tokens: list
:param tokens: tokens from executed 'help' command.
:type tree: object
:param tree: 'current' class or method during recursion
"""
if tree is None:
return []
elif len(tokens) == 0:
return members(tree)
if len(tokens) == 1:
return complete_list(set(members(tree, 'help_')) | set(members(tree)), tokens[0])
elif tokens[0] in members(tree):
return cls._traverse_help(tokens[1:],
getattr(tree, 'do_' + tokens[0]))
return []
@staticmethod
[docs] def call_static(func, *args, **kwargs):
"""Call a method defined using @staticmethod.
Because we want to define completion functions in their associated class
and we want them to be static methods we cannot call them directly. Make
sure a callable object is called.
"""
try:
return func(*args, **kwargs)
except TypeError:
try:
return func.__func__(*args, **kwargs)
except AttributeError:
# py2.6 doesn't have __func__ for staticmethods
return func.__get__(True)(*args, **kwargs)
# traverse_do is recursive so needs to find itself through the class
@classmethod
def _traverse_do(cls, tokens, tree):
"""Traverse through the class tree of do_*() methods to find a do_*()
method whose completions function is called to give a list of possible
arguments or subcommands.
:type tokens: list
:param tokens: tokens from executed 'help' command.
:type tree: object
:param tree: 'current' class or method during recursion
"""
if inspect.isclass(tree):
tree = tree()
if tree is None:
return []
elif len(tokens) == 0:
return members(tree)
if len(tokens) == 1:
if hasattr(tree, 'completions'):
return (c for f in tree.completions for c in cls.call_static(f, tokens[0]))
return complete_list(members(tree), tokens[0])
if tokens[0] in members(tree):
return cls._traverse_do(tokens[1:],
getattr(tree, 'do_' + tokens[0]))
if hasattr(tree, 'completions'):
return (c for f in tree.completions for c in cls.call_static(f, tokens[-1]))
return []
@rl.generator
def complete(self, text):
"""Return a list of possible completions from the line currently entered
at the prompt. If the first word is "help", try to find a help_*()
method through _traverse_do, otherwise look for a command through
_traverse_do().
:type text: string
:param text: line entered at prompt
:return: list
"""
endidx = readline.get_endidx()
buf = readline.get_line_buffer()
tokens = buf[:endidx].split()
if not tokens or buf[endidx - 1] == ' ':
tokens.append('')
if tokens[0] == "help":
return self._traverse_help(tokens[1:], self)
else:
return self._traverse_do(tokens, self)
[docs] def cancel(self, prompt=False):
"""Update the shell to indicate a 'cancel'.
:type prompt: boolean
:param prompt: If True, force a redraw of the prompt & line.
"""
self.stdout.write(str(" ^C") + "\n")
readline.replace_line("")
if prompt:
readline.redisplay(True)