2005-10-04 06:04:23 +00:00
|
|
|
# -*- coding: iso-8859-1 -*-
|
|
|
|
|
|
|
|
import os, re, binascii, sys, exceptions, traceback, inspect
|
|
|
|
|
|
|
|
try:
|
|
|
|
import readline
|
|
|
|
except ImportError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class Shell:
|
|
|
|
"""This class handles a shell with all the usual goodies: history,
|
|
|
|
tab-completion, start-up script. The readline module will be used when
|
|
|
|
available to provide some of the functionality."""
|
|
|
|
|
|
|
|
def __init__(self, basename):
|
|
|
|
"""Creates a new shell object with the specified basename. The
|
|
|
|
basename will be used when constructing the history and start-up script
|
|
|
|
filenames: ~/.basename.history and ~/.basenamerc.
|
|
|
|
|
|
|
|
Note that the start-up script will not be called at instantiation time
|
|
|
|
but when starting the main-loop. This is so that you can register your
|
|
|
|
own commands first."""
|
|
|
|
|
|
|
|
if sys.modules.has_key("readline"):
|
|
|
|
histfile = os.path.join(os.environ["HOME"], ".%s.history" % basename)
|
|
|
|
try:
|
|
|
|
readline.read_history_file(histfile)
|
|
|
|
except IOError:
|
|
|
|
pass
|
|
|
|
import atexit
|
|
|
|
atexit.register(readline.write_history_file, histfile)
|
|
|
|
del histfile
|
|
|
|
|
|
|
|
readline.parse_and_bind("tab: complete")
|
|
|
|
## FIXME basenamerc
|
2005-10-07 13:11:57 +00:00
|
|
|
|
|
|
|
readline.set_completer(self.complete)
|
2005-10-08 23:34:10 +00:00
|
|
|
readline.set_completer_delims("")
|
2005-10-04 06:04:23 +00:00
|
|
|
else:
|
|
|
|
print >>sys.stderr, "Warning: No readline module available. Most functionality will be missing."
|
|
|
|
|
|
|
|
self._commandsets = [] ## This contains command sets, it's a list of (object, dictionary) tuples
|
|
|
|
self.basename = basename
|
2005-10-07 01:50:13 +00:00
|
|
|
self.env = {"print_backtrace": "true"}
|
2005-10-04 06:04:23 +00:00
|
|
|
|
|
|
|
self.register_commands(self)
|
2006-11-21 00:38:05 +00:00
|
|
|
self.startup_ran = False
|
2005-10-07 01:50:13 +00:00
|
|
|
self.fallback = None
|
|
|
|
self.pre_hook = []
|
|
|
|
self.post_hook = []
|
|
|
|
self.prompt = ""
|
2005-10-04 06:04:23 +00:00
|
|
|
|
2005-10-07 01:50:13 +00:00
|
|
|
def get_prompt(self):
|
|
|
|
return self.prompt
|
|
|
|
def set_prompt(self, prompt):
|
|
|
|
self.prompt = prompt
|
|
|
|
|
|
|
|
def register_pre_hook(self, function):
|
|
|
|
self.pre_hook.append(function)
|
|
|
|
|
|
|
|
def register_post_hook(self, function):
|
|
|
|
self.post_hook.append(function)
|
|
|
|
|
|
|
|
def unregister_pre_hook(self, function):
|
|
|
|
self.pre_hook.remove(function)
|
|
|
|
|
|
|
|
def unregister_post_hook(self, function):
|
|
|
|
self.post_hook.remove(function)
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
"""Runs a loop to read commands and execute them. This function does
|
|
|
|
not (normally) return."""
|
|
|
|
|
2006-11-21 00:38:05 +00:00
|
|
|
if not self.startup_ran:
|
|
|
|
self.run_startup()
|
|
|
|
|
|
|
|
self._run()
|
|
|
|
|
|
|
|
def run_startup(self):
|
2006-11-19 12:18:01 +00:00
|
|
|
lines = []
|
2006-11-21 00:38:05 +00:00
|
|
|
self.startup_ran = True
|
2006-11-19 12:18:01 +00:00
|
|
|
try:
|
|
|
|
fp = file(os.path.join(os.environ["HOME"], ".%src" % self.basename))
|
|
|
|
lines = fp.readlines()
|
|
|
|
fp.close()
|
|
|
|
except IOError:
|
2006-11-21 00:38:05 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
self._run(lines)
|
|
|
|
|
|
|
|
def _run(self, lines = None):
|
|
|
|
|
|
|
|
line = ""
|
2005-10-07 01:50:13 +00:00
|
|
|
|
2006-11-21 00:38:05 +00:00
|
|
|
while lines is None or len(lines) > 0:
|
2005-10-07 01:50:13 +00:00
|
|
|
try:
|
2005-10-07 13:11:57 +00:00
|
|
|
for function in self.pre_hook:
|
|
|
|
function()
|
|
|
|
|
2006-11-21 00:38:05 +00:00
|
|
|
if lines is not None and len(lines) > 0:
|
2006-11-19 12:18:01 +00:00
|
|
|
line = lines.pop(0)
|
|
|
|
else:
|
|
|
|
line = raw_input("%s> " % self.prompt)
|
2005-10-07 13:11:57 +00:00
|
|
|
|
2005-10-07 01:50:13 +00:00
|
|
|
except EOFError:
|
|
|
|
print ## line break (there probably was none after the prompt)
|
|
|
|
break
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
print ## only clear the current command
|
2006-11-19 12:18:01 +00:00
|
|
|
continue
|
2005-10-07 01:50:13 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
self.parse_and_execute(line)
|
2005-10-09 02:05:19 +00:00
|
|
|
|
|
|
|
for function in self.post_hook:
|
|
|
|
function()
|
2005-10-07 01:50:13 +00:00
|
|
|
except Exception:
|
|
|
|
exctype, value = sys.exc_info()[:2]
|
|
|
|
if exctype == exceptions.SystemExit:
|
|
|
|
raise exctype, value
|
|
|
|
print "%s: %s" % (exctype, value)
|
|
|
|
if self.env.get("print_backtrace", "") != "":
|
|
|
|
traceback.print_tb(sys.exc_info()[2])
|
|
|
|
|
2005-10-08 23:34:10 +00:00
|
|
|
_commandregex = re.compile(r'\s*(\w+)(\s+.*)?')
|
2006-11-08 06:39:54 +00:00
|
|
|
_argumentregex = re.compile(r"""\s*(?:"((?:[^"]|\\"|\\\\)*)"|'([^']*)'|(\S+))(\s+\S.*)?""")
|
2005-10-07 01:50:13 +00:00
|
|
|
def parse_and_execute(self, line):
|
|
|
|
"""Parses a command line and executes the associated function."""
|
|
|
|
match = self._commandregex.match(line)
|
|
|
|
if not match:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
command = match.group(1)
|
|
|
|
argstring = match.group(2) and match.group(2).strip() or ""
|
|
|
|
|
2005-10-07 12:35:06 +00:00
|
|
|
function = None
|
|
|
|
object = None
|
|
|
|
args = []
|
2005-10-07 01:50:13 +00:00
|
|
|
command_mapping = self.get_command_mapping()
|
2005-10-07 12:35:06 +00:00
|
|
|
|
|
|
|
if command_mapping.has_key(command):
|
2005-10-07 01:50:13 +00:00
|
|
|
command_set = command_mapping[command]
|
|
|
|
object = command_set[0] ## Implicit first argument, if set
|
|
|
|
function = command_set[1][command] ## The actual function to call
|
|
|
|
|
2005-10-07 12:35:06 +00:00
|
|
|
if object is not None:
|
|
|
|
args.append(object)
|
|
|
|
|
|
|
|
else:
|
|
|
|
if self.fallback is None:
|
|
|
|
print "Unknown command '%s'. Try 'help' to list known commands." % command
|
|
|
|
else:
|
|
|
|
## Fall back to the fallback function/method
|
|
|
|
## It will receive the command executed as first parameter
|
|
|
|
args.append(command)
|
|
|
|
function = self.fallback
|
|
|
|
object = None ## fallback must be a function or a bound method
|
|
|
|
|
|
|
|
if function is not None:
|
2005-10-07 01:50:13 +00:00
|
|
|
(argnames, varargs, varkw, defaults) = \
|
|
|
|
inspect.getargspec(function)
|
|
|
|
|
|
|
|
## minimum number of argument the function accepts
|
2005-10-09 01:38:06 +00:00
|
|
|
args_needed = len(argnames) - len(args) - (defaults and len(defaults) or 0)
|
|
|
|
## maximum number of arguments the function accepts
|
|
|
|
args_possible = varargs is None and (len(argnames) - len(args)) or None
|
2005-10-07 01:50:13 +00:00
|
|
|
|
2005-10-07 12:35:06 +00:00
|
|
|
args_so_far = 0
|
|
|
|
|
2005-10-07 01:50:13 +00:00
|
|
|
while len(argstring) > 0:
|
|
|
|
match = self._argumentregex.match(argstring)
|
|
|
|
if not match:
|
|
|
|
break
|
|
|
|
else:
|
2005-10-07 12:35:06 +00:00
|
|
|
args_so_far = args_so_far + 1
|
2005-10-07 01:50:13 +00:00
|
|
|
current_arg = match.group(1) or match.group(2) or match.group(3) or ""
|
|
|
|
argstring = match.group(4) or ""
|
|
|
|
args.append(current_arg)
|
|
|
|
|
2005-10-07 12:35:06 +00:00
|
|
|
if args_so_far < args_needed:
|
|
|
|
print "The %s command takes at least %i arguments. You gave %i." % (command, args_needed, args_so_far)
|
|
|
|
return
|
2005-10-09 01:38:06 +00:00
|
|
|
if args_possible is not None and args_so_far > args_possible:
|
2005-10-07 12:35:06 +00:00
|
|
|
print "The %s command takes at most %i arguments. You gave %i." % (command, args_possible, args_so_far)
|
|
|
|
return
|
|
|
|
|
2005-10-09 01:38:06 +00:00
|
|
|
try:
|
|
|
|
return function(*args)
|
|
|
|
except NotImplementedError:
|
|
|
|
print "Unknown command '%s'. Try 'help' to list known commands." % command
|
2005-10-07 13:11:57 +00:00
|
|
|
|
|
|
|
def complete(self, line, state):
|
|
|
|
"""Try to complete a command line. Known command names first,
|
|
|
|
then programmable completion (if applicable)."""
|
|
|
|
|
|
|
|
match = self._commandregex.match(line)
|
|
|
|
if not match:
|
|
|
|
groups = (None, None)
|
|
|
|
else:
|
|
|
|
groups = match.groups()
|
|
|
|
|
|
|
|
found = -1
|
|
|
|
if groups[1] is None:
|
|
|
|
## (Possibly incomplete) command
|
|
|
|
command_to_match = groups[0] or ""
|
|
|
|
retval = False
|
|
|
|
|
|
|
|
for cmdset in self._commandsets:
|
|
|
|
for (command, function) in cmdset[1].items():
|
|
|
|
if command[:len(command_to_match)] == command_to_match:
|
|
|
|
found = found + 1
|
|
|
|
if found == state:
|
|
|
|
retval = command
|
|
|
|
|
|
|
|
## We break when we know there are at least 2 matches
|
|
|
|
## and the correct match according to the current state
|
|
|
|
## has been reached. Normally it would be enough
|
|
|
|
## to break when found == state, but we want to know
|
|
|
|
## if this is the only match, so that we can append a
|
|
|
|
## space
|
|
|
|
if found > 0 and found >= state:
|
|
|
|
break
|
|
|
|
|
|
|
|
if found > 0 and found >= state:
|
|
|
|
break
|
|
|
|
|
|
|
|
if found == 0 and sys.modules.has_key("readline"):
|
|
|
|
## There is exactly one command that matches. Append a space
|
|
|
|
## after it
|
|
|
|
return retval + " "
|
|
|
|
return retval
|
|
|
|
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return False
|
2005-10-07 01:50:13 +00:00
|
|
|
|
|
|
|
|
2005-10-04 06:04:23 +00:00
|
|
|
def _make_cmdset(target, commands):
|
|
|
|
"""Convenience function for code shared between register_commands
|
|
|
|
and unregister_commands."""
|
|
|
|
|
|
|
|
if commands is None:
|
|
|
|
if isinstance(target, dict):
|
|
|
|
new_target = None
|
|
|
|
new_commands = target
|
|
|
|
elif hasattr(target, "COMMANDS"):
|
|
|
|
new_target = target
|
|
|
|
new_commands = target.COMMANDS
|
|
|
|
else:
|
|
|
|
raise TypeError, "target must be either an object with a COMMANDS attribute or a dictionary, not %s" % type(target)
|
|
|
|
else:
|
|
|
|
if isinstance(commands, dict):
|
|
|
|
new_target = target
|
|
|
|
new_commands = commands
|
|
|
|
else:
|
|
|
|
raise TypeError, "commands must be a dictionary, not %s" % type(commandset)
|
|
|
|
|
|
|
|
return (new_target, new_commands)
|
|
|
|
_make_cmdset = staticmethod(_make_cmdset)
|
|
|
|
def register_commands(self, target, commands=None):
|
|
|
|
"""Register an object to provide commands.
|
|
|
|
When commands is None or not given then target must either be
|
|
|
|
an object with a COMMANDS attribute or a dictionary mapping command
|
|
|
|
strings to functions. When commands is given then target can be any
|
|
|
|
object and commands must be a dictionary mapping command strings to
|
|
|
|
functions."""
|
|
|
|
|
|
|
|
new_commandset = self._make_cmdset(target, commands)
|
|
|
|
current_commands = self.get_command_mapping()
|
|
|
|
|
|
|
|
for (command,function) in new_commandset[1].items():
|
|
|
|
if not hasattr(function, "__doc__"):
|
|
|
|
print >>sys.stderr, "Warning: function %s does not have a docstring, bug author" % function
|
|
|
|
old_commandset = current_commands.get(command)
|
|
|
|
if old_commandset is not None:
|
|
|
|
print >>sys.stderr, "Warning: command '%s' already defined from %s, new definition from %s" % (
|
|
|
|
command, old_commandset[0] or "Anonymous list", new_target or "Anonymous list"
|
|
|
|
)
|
|
|
|
|
|
|
|
self._commandsets.append( new_commandset )
|
|
|
|
|
2006-05-22 03:24:15 +00:00
|
|
|
def unregister_commands(self, target, commands=None):
|
2005-10-04 06:04:23 +00:00
|
|
|
"""Unregister an object to provide commands.
|
|
|
|
You should provide the same parameters as in the call to
|
|
|
|
register_commands()."""
|
|
|
|
|
|
|
|
old_commandset = self._make_cmdset(target, commands)
|
|
|
|
return self._commandsets.remove( old_commandset )
|
|
|
|
|
|
|
|
def get_command_mapping(self):
|
|
|
|
"""Returns a dictionary that maps commands to their commandsets."""
|
|
|
|
commands = {}
|
|
|
|
for cmdset in self._commandsets:
|
|
|
|
for (command, function) in cmdset[1].items():
|
|
|
|
commands[command] = cmdset
|
|
|
|
return commands
|
|
|
|
|
|
|
|
def has_command(self, name):
|
|
|
|
"""Returns whether this shell knows about a specified command."""
|
|
|
|
for cmdset in self._commandsets:
|
|
|
|
for (command, function) in cmdset[1].items():
|
|
|
|
if command == name:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def help(self, name):
|
|
|
|
"""Return a dictionary with help about command named name. The dictionary
|
|
|
|
keys are: name, formatted_parameters, description, long_description"""
|
|
|
|
|
|
|
|
for cmdset in self._commandsets:
|
|
|
|
for (command, function) in cmdset[1].items():
|
|
|
|
if command == name:
|
|
|
|
parts = function.__doc__.split("\n", 1)
|
|
|
|
if len(parts) != 2:
|
|
|
|
parts = [ e.strip() for e in (parts + ["",""])[:2] ]
|
|
|
|
|
|
|
|
(argnames, varargs, varkw, defaults) = \
|
|
|
|
inspect.getargspec(function)
|
|
|
|
if argnames[0] == "self": ## Any better way?
|
|
|
|
argnames = argnames[1:]
|
|
|
|
|
|
|
|
len_mandatory = len(argnames) - \
|
|
|
|
len(defaults or [])
|
|
|
|
argstring = " ".join(
|
|
|
|
[(i < len_mandatory and "%s" or "[%s]")
|
|
|
|
% argnames[i] for i in range(len(argnames))]
|
|
|
|
)
|
|
|
|
|
|
|
|
return { "name": command,
|
|
|
|
"formatted_parameters": argstring,
|
|
|
|
"description": parts[0],
|
|
|
|
"long_description": parts[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
raise ValueError, "No such command '%s'" % name
|
|
|
|
|
|
|
|
def cmd_exit(self):
|
|
|
|
"Exit the shell."
|
|
|
|
sys.exit(0)
|
|
|
|
|
2005-10-07 01:50:13 +00:00
|
|
|
SETTINGS_FORMATSTRING="%s=%s"
|
2005-10-04 06:04:23 +00:00
|
|
|
def cmd_set(self, name=None, value=None):
|
|
|
|
"Set a variable or print current settings."
|
2005-10-07 01:50:13 +00:00
|
|
|
|
|
|
|
if name == None and value == None:
|
|
|
|
for (name, value) in self.env.items():
|
|
|
|
print self.SETTINGS_FORMATSTRING % (name, value)
|
|
|
|
elif name is not None and value is not None:
|
|
|
|
self.env[name] = value
|
|
|
|
else:
|
|
|
|
raise ValueError, "Need either name and value, or no parameters at all."
|
2005-10-04 06:04:23 +00:00
|
|
|
|
2005-10-07 12:35:06 +00:00
|
|
|
def cmd_unset(self, name):
|
|
|
|
"""Unset a variable."""
|
|
|
|
if self.env.has_key(name):
|
|
|
|
del self.env[name]
|
|
|
|
|
2005-10-04 06:04:23 +00:00
|
|
|
SHORT_HELP_FORMATSTRING = "%(name)-20s %(description)s"
|
|
|
|
LONG_HELP_FORMATSTRING = "%(description)s\nSynopsis: %(name)s %(formatted_parameters)s\n%(long_description)s"
|
|
|
|
def cmd_help(self, command=None):
|
|
|
|
"Print help, either for all commands or for a specific one."
|
|
|
|
if command is None:
|
|
|
|
command_list = self.get_command_mapping().keys()
|
2006-05-25 13:36:25 +00:00
|
|
|
command_list.sort()
|
2005-10-04 06:04:23 +00:00
|
|
|
for command in command_list:
|
|
|
|
print self.SHORT_HELP_FORMATSTRING % self.help(command)
|
|
|
|
else:
|
|
|
|
if self.has_command(command):
|
|
|
|
print self.LONG_HELP_FORMATSTRING % self.help(command)
|
|
|
|
else:
|
|
|
|
print "No such command '%s'" % command
|
|
|
|
|
|
|
|
COMMANDS = {
|
|
|
|
"exit": cmd_exit,
|
|
|
|
"set": cmd_set,
|
2005-10-07 12:35:06 +00:00
|
|
|
"unset": cmd_unset,
|
2005-10-04 06:04:23 +00:00
|
|
|
"help": cmd_help
|
|
|
|
}
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
s = Shell("foobar")
|
2005-10-07 01:50:13 +00:00
|
|
|
s.run()
|
2005-10-04 06:04:23 +00:00
|
|
|
|