Another Unix shell in Python
Published: October 04, 2008Tags: python unix programming
A while ago I posted the code for a simple Unix shell written in Python. Not too long after this I expressed my delight at discovering the Python standard library's cmd module. It turns out that these two concepts go together really well. Here's a much nicer simple Unix shell, less than 100 lines long. It still lacks input/output redirection or pipes, but by virtue of subclassing Cmd it has gained a command line history and tab completion. The tab completion is not the smoothest you'll ever see - this is basically the first thing I could get almost working - but I think this solidly demonstrates that Cmd makes a fine base for a Pythonic Unix shell.
from cmd import Cmd
from glob import glob
from os import access, chdir, execv, fork, getenv, getcwd, listdir, wait, X_OK
from os.path import exists, isdir
from sys import exit
class Shell(Cmd):
def _substitute_env_var(self, token):
if token.startswith("$"):
return getenv(token[1:], "")
else:
return token
def precmd(self, line):
words = line.split()
words = map(self._substitute_env_var, words)
return " ".join(words)
def _get_executables(self):
result = []
for dir in getenv("PATH").split(":"):
try:
result.extend(filter(lambda(x): access(dir+"/"+x,X_OK), listdir(dir)))
except OSError:
pass
return result
def postcmd(self, stop, line):
self.prompt = getenv("PS1", "$ ")
self.executables = self._get_executables()
if stop:
return True
def completenames(self, text, *ignored):
return filter(lambda(x): x.startswith(text), self.executables)
def completedefault(self, text, line, begidx, endidx):
prefix = line.split()[-1]
return map(lambda(x): x[len(prefix)-len(text):], glob(prefix+"*"))
def do_cd(self, line):
if not line:
line = getenv("HOME", "/")
try:
chdir(line.split()[0])
except OSError, (errno, strerror):
if errno == 2:
print "Directory %s does not exist." % line.split()[0]
def complete_cd(self, text, line, begidx, endidx):
prefix = line.split()[-1]
return map(lambda(x): x[len(prefix)-len(text):],filter(lambda(x): isdir(x), glob(prefix+"*")))
def do_cwd (self, line):
print getcwd()
def do_EOF(self, line):
print ""
return True
def default(self, line):
argv = line.split()
command = argv[0]
found = False
path = getenv("PATH")
for dir in path.split(":"):
if exists("/".join((dir, command))):
found = True
if(fork() == 0):
execv("/".join((dir, command)), argv)
else:
wait()
if not found:
print "%s: Not found" % command
shell = Shell()
shell.prompt = getenv("PS1", "$ ")
shell.executables = shell._get_executables()
shell.cmdloop()
Lately I've been trying to make an effort to use Python's functional programming features, like map and filter, instead of cumbersome piles of fors, ifs and appends, which is evident in this code.