Mittwoch, 14. Dezember 2011

cmd - Python-Modul für text-basierte Programme

Wer (mit Python) programmiert kommt irgendwann an den Punkt, wo das Programm von Nutzer Daten erwartet. Die kann man noch als Option beim Aufruf des Programms mitgeben, wobei das bei vielen Werten / Optionen etwas mühselig wird. Und wenn das Programm mit dem Nutzer interagieren will, dann muss man schon Tastatureingaben abfragen und verarbeiten. Das geht z.B. mit input bzw. raw_input im DIY-Verfahren.

Aber bekanntlich hat Python ja "Batteries included" und bringt in der Standardinstallation für eine solche Anwendung bereits ein eigenes Modul Names "cmd" mit. Dieses stellt recht komfortabel ein Rahmenwerk bereit, um Befehle auf der Kommandozeile entgegen zu nehmen und an die passende Funktion weiter zu reichen, welche dann das Ergebnis ausgibt. Danach erscheint ein neuer Prompt. Dies geschieht so lange, bis man das Programm beendet, wobei das cmd-Modul auch hier eine Weg bietet, ein "sauberes" Programmende durchzuführen.

Im folgenden Skript wird eine kleine Telefonbuchapplikation mit Hilfe des cmd-Moduls realisiert. Dabei kann einem Namen einfach nur eine Telefonnummer zugeordnet werden. Zur persistenten Speicherung kommt das Dumb-DBM Python-Modul zum Einsatz. Auf die Daten kann so wie auch ein normales Python Dictionary zugegriffen werden, wobei die Daten als "Name: Nummer" Wertpaar abgelegt werden.

Das Programm hat drei Funktionen. Man kann

  • eine Eintrag hinzufügen
  • einen Eintrag anzeigen lassen bzw. alle Einträge anzeigen lassen
  • einen Eintrag löschen
Beim Hinzufügen von Namen mit Nummer erwartet das Programm, dass diese durch das Pipe-Zeichen | getrennt sind.

Doppelte Einträge für den Namen sind nicht erlaubt.

Das entsprechende Skript sieht so aus:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cmd, sys, dumbdbm

class Telefonbuch(cmd.Cmd):
   
    prompt = '(Eingabe): '
   
    def __init__(self,db_name):
        cmd.Cmd.__init__(self)    
        self.db_name = db_name
        self.db = None
   
    def preloop(self):
        self.db = dumbdbm.open(self.db_name)
        print u'Nutze die Datenbank {0}'.format(db_name)
        print u'Die Datenbank hat aktuell {0} Einträge.'.format(len(self.db))
        print
       
    def postloop(self):
        self.db.close()
        print u'Datenbank geschlossen...'
   
    def do_zeige(self, line):
        if len(self.db) == 0:
            print u'Die Datenbank ist leer...'
        if line:
            if not self.db.has_key(line):
                print u'Die Datenbank hat keinen Eintrag {0}'.format(line)
            else:
                print u'{0}: {1}'.format(line,self.db[line])
        else:
            for k,v in self.db.iteritems():
                print u'{0}: {1}'.format(k.decode('utf-8'),v)

    def help_zeige(self):
        print '\n'.join([u'zeige NAME',
            u'Zeigt die Telefonnummer für NAME an.',
            u'Wird kein NAME angegeben, werden alle Einträge angezeigt'])
   
    def do_dazu(self, line):
        eintrag = line.split('|')
        if len(eintrag) != 2:
            msg = '\n'.join(
                [u'Fehler bei der Eingabe!',
                 u'Name und Nummer bitte durch Pipe-Zeichen trennen!'])
            print msg
            return
        if self.db.has_key(eintrag[0].strip()):
            print u'Es gibt schon einen Eintrag mit dem Namen {0}'.format(
                eintrag[0])
            return
        self.db[eintrag[0].strip()] = eintrag[1].strip()
   
    def do_weg(self, line):
        if not self.db.has_key(line):
            print u'Die Datenbank hat keinen Eintrag {0}'.format(
                line.decode('utf-8'))
        else:
            nummer = self.db.pop(line)
            print u'Namen {0} mit Nummer {1} wurde gelöscht'.format(
                line.decode('utf-8'),nummer)
   
    def do_ende(self, line):
        print 'Programm beenden...'
        return True
   
    def do_EOF(self, line):
        print 'Programm beenden...'
        return True

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print u'Angabe der Datenbankdatei fehlt!'
        sys.exit()
    else:
        db_name = sys.argv[1]
        telefonbuch = Telefonbuch(db_name)
        telefonbuch.cmdloop()

Die Applikation besteht aus einer Klasse namens Telefonbuch, welche von cmd.Cmd erbt. Da hier der Name der Datenbank sowie eine Instanz der Datenbank als Attribut der Klasse genutzt wird, muss auch cmd.Cmd in __init__ aufgerufen werden. Hat die eigene Klasse keine __init__-Funktion, dann ist dieser Schirtt nicht notwendig. Wie zu sehen ist, erwartet die Klassen Telefonbuch ein Parameter beim Aufruf, nämlich den Namen der Datenbank, welche für das Telefonbuch verwendet werden soll.

Der Aufruf des Programms erfolgt immer über das Schema NAME_DER_KLASSE.cmdloop(). Dadurch wird solange ein Eingabeprompt ausgegeben und die eingegebenen Befehle verarbeitet, bis das Programm beendet wird.

Werfen wir nun einen Blick in die Klasse. Als erstes wird mit prompt = '(Eingabe)' ein eigenes Prompt definiert. Standardmäßig würde sonst (Cmd) angezeigt. Es folgen zwei Methoden namens preloop und postloop. Diese sind in cmd bereits vorhanden, dienen aber dazu, überschrieben zu werden. Wie die Namen schon vermuten lassen, werden die Funktionen ausgeführt, bevor der Hauptloop der Anwendung gestartet wird bzw. bevor das Programm beendet wird. Hier werden die Funktionen dazu genutzt, um die Datenbank zu öffnen (bzw., falls die Datenbank noch nicht existiert, anzulegen) und wieder regulär zu schließen. Es gibt übrigens auch die Funktionen precmd und postcmd - welche hier nicht verwendet werden - die jedesmal vor und nach der Ausführung eines Befehls aufgerufen werden. Die Verwendung von preloop, precmd, postcmd und postloop ist dabei optional, d.h. sie müssen nicht im Programm enthalten sein.

Das Schema von cmd zum Definieren von Befehlen innerhalb der Applikation ist recht simple, wie zu erkennen ist. Jeder Befehl wird über do_BEFEHL festgelegt. In dieser Applikation gibt es also die Befehle zeige, dazu, weg und ende, welche entsprechend in den Funktionen do_zeige, do_dazu, do_weg und do_ende definiert sind. Weiterhin gibt es do_EOF, welche durch einen Druck der Tastenkombination STRG+D die Applikation regulär beendet. do_EOF sollte in jeder Applikation enthalten sein. Die Funktionen können beliebigen Programmcode enthalten, mit print wird in die Standardausgabe geschrieben. Ist eine Funktion durchlaufen, erscheint ein neuer Prompt.

Alle Befehle erhalten die zusätzlich eingegebenen Parameter über die Variable line. Wird z.B. hier der Befehl dazu Jochen | 12345 eingegeben, wird die Funktion dazu aufgerufen und Jochen | 12345 wird in line hinterlegt. Beim Programmieren sollte man immer im Hinterkopf behalten, dass das cmd-Modul nur das Rahmenwerk bereit stellt und keinerlei Eingabefehler etc. abfängt. Darum muss sich der Programmierer selber kümmern.

Weiterhin besitzt das cmd-Modul einen Befehl, um Hilfstext zu den Funktionen anzuzeigen. Diese müssen natürlich explizit angelegt werden, so wie hier beispielhaft für die Funktion zeige. Hilfstexte werden immer nach dem Schema help_BEFEHL angelegt. Die Hilfe lässt sich durch die Eingabe von help ohne weitere Parameter am Prompt aufrufen.

Wie zu sehen ist, ist die Verwendung des cmd-Moduls recht einfach. Mit minimalen Aufwand kann man eine interaktive Terminalanwendung schreiben. Diese ist portable, d.h. sie sollte gleichermaßen und Linux, Windows und MacOS laufen.

weiterführende Links:


Keine Kommentare:

Kommentar veröffentlichen