Another Unix shell in Python

Published: October 04, 2008
Tags: 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.

Feeds
Archives
Top tags