[The code for all this is here: pii.py]
Update: I've only tried this under Linux; the special commands doesn't seem to work under IDLE. Also, you need to execfile pii.py, and not import it...
I've written some code to help the interactive interpreter accept simple commands. Commands like "ls" and those like "cd ~/tmp". Commands without arguments -- simple commands -- can be Python objects. But for those with arguments (special commands), we need a little magic: when there is a space between a command and its argument ("cd ~/tmp"), Python raises a SyntaxError. And the SyntaxError contains the problematic line:
>>> try:
... eval('a b')
... except SyntaxError, s:
... print repr(s.text)
...
'a b'
So we simply "catch" the error and extract the command that needs to be executed and execute it. The two hook functions, sys.displayhook and sys.excepthook are necessary to make this happen. sys.displayhook is needed to display the simple commands & sys.excepthook is needed to extract out the special commands and run them. We replace both the hooks like so:
def mydisplayhook(value):
if value is not None:
# this'll only invoke simplecmds (or
# cmds that install themselves in
# __builtins__) -- the specialcmds are
# executed in myexcepthook
if isinstance(value, SimpleCmd):
out = value()
if out:
print out
else:
__builtins__._ = value
print repr(value)
sys.displayhook = mydisplayhook
def myexcepthook(type, value, tb):
# imports giving rise to syntaxerror shouldn't
# be considered a s cmds
if type is SyntaxError and value.filename=='<stdin>':
# might be a cmd of ours
line = value.text
# if user is entering a multiline expr
# (line[0]!=' ') -- they indent everything
# other than first line -- don't consider
# that a cmd
if line and line[0] not in ' \t':
cmd = value.text.split()[0]
if cmd in specialcmds:
fn = specialcmds[cmd]
arg = line[len(cmd):]
fn(arg)
return
traceback.print_exception(type, value, tb)
sys.excepthook = myexcepthook
Two points:
- Special commands receive a single string -- it is up to the command to split it into multiple arguments, if necessary
- also, simple commands sit in the __builtins__ namespace, so even if you replace it, you can get it back by saying "del command-name")
Here is the whole thing. It sits in my pythonrc file. You can download it here.
import sys, os, datetime, traceback, commands
specialcmds = {}
allcmds = []
class CmdMeta(type):
def __init__(cls, name, bases, dict):
super(CmdMeta, cls).__init__(cls, name, bases, dict)
if name == 'Cmd': return
simplecmds = {}
specials = {}
inst = cls()
inst.install(specials, simplecmds)
for k in simplecmds:
setattr(__builtins__, k, SimpleCmd(simplecmds[k]))
allcmds.append((name, cls, inst, simplecmds, specials))
specialcmds.update(specials)
class Cmd(object):
__metaclass__ = CmdMeta
class SimpleCmd(object):
def __init__(self, func):
self.func = func
def __call__(self):
result = self.func()
if result:
return str(result)
def myexcepthook(type, value, tb):
# imports giving rise to syntaxerror shouldn't
# be considered as cmds
if type is SyntaxError and value.filename=='<stdin>':
# might be a cmd of ours
line = value.text
# if user is entering a multiline expr
# (line[0]!=' ') -- they indent everything
# other than first line -- don't consider
# that a cmd
if line and line[0] not in ' \t':
cmd = value.text.split()[0]
if cmd in specialcmds:
fn = specialcmds[cmd]
arg = line[len(cmd):]
fn(arg)
return
traceback.print_exception(type, value, tb)
sys.excepthook = myexcepthook
# reloading this file shouldn't mess up prev value
try:
__builtins__._
except AttributeError:
__builtins__._ = None
def mydisplayhook(value):
if value is not None:
# this'll only invoke simplecmds (or
# cmds that install themselves in
# __builtins__) -- the specialcmds are
# executed in myexcepthook
if isinstance(value, SimpleCmd):
out = value()
if out:
print out
else:
__builtins__._ = value
print repr(value)
sys.displayhook = mydisplayhook
And here are some commands:
#
# Commands:
#
class MyCmds(Cmd):
def install(self, specialcmds, simplecmds):
simplecmds['cmds'] = self.allcmds
def doc(self, x):
return getattr(x, '__doc__') or '(no doc)'
def allcmds(self):
"prints out all the cmds"
print "all commands\n", 65*'='
cmds = []
for (name, cls, inst, simplecmds, specialcmds) in allcmds:
simple = set(simplecmds.keys())
specials = set(specialcmds.keys())
both = simple & specials
simple -= both
specials -= both
for k in both:
cmds.append((k, 'both', self.doc(simplecmds[k])))
for k in specials:
cmds.append((k, 'special', self.doc(specialcmds[k])))
for k in sorted(simple):
cmds.append((k, 'simple', self.doc(simplecmds[k])))
cmds.sort()
for x in cmds:
print '%-6s %-7s %s' % x
class MyReload(Cmd):
def install(self, specialcmds, simplecmds):
simplecmds['rl'] = self.doit
def doit(self):
"reloads your pythonrc startup file"
path = os.environ.get('PYTHONSTARTUP')
if not path:
print "can't find your pythonstartup"
else:
execfile(path, globals())
class MyPwd(Cmd):
def install(self, specialcmds, simplecmds):
simplecmds['pwd'] = simplecmds['cwd'] = self.getcwd
def getcwd(self):
"prints the current working directory"
return os.getcwd()
class MyCD(Cmd):
def __init__(self):
self.popdirs = []
def install(self, specialcmds, simplecmds):
simplecmds['popd'] = self.popd
simplecmds['cd'] = specialcmds['cd'] = self.cd
def popd(self):
"changes back to directory from which we came"
if self.popdirs:
self.cd(self.popdirs.pop(), False)
else:
print 'nothing to pop back to'
def cd(self, dir=None, insert=True):
"changes to a given directory or user home; resolves ~ to user home"
if not dir:
import user
dir = user.home
dir = os.path.expanduser(dir.strip())
if not os.path.isdir(dir):
print 'not a dir:', dir
else:
if insert:
self.popdirs.append(os.getcwd())
os.chdir(dir)
print 'changed to', dir
class MyHelp(Cmd):
def install(self, specialcmds, simplecmds):
specialcmds['h'] = self.help
def help(self, x):
"prints help to a given thing"
help(x.strip())
class MyLs(Cmd):
def install(self, specialcmds, simplecmds):
specialcmds['ls'] = self.ls
simplecmds['ls'] = self.ls
def ls(self, dir=''):
"""run the unix command 'ls -l'"""
dir = os.path.expanduser(dir.strip() or '.')
print commands.getoutput('ls -l %s' % dir)