Früh übt sich - das Telefonbuch - Axel-Tobias Schreiner,Universität Osnabrück
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
Früh übt sich — das Telefonbuch Axel-Tobias Schreiner, Universität Osnabrück Hat man bequemen Zugriff auf ein UNIX-System, so kann man dort natürlich auch Adressen und Telefonnummern notieren. Schreibt man Briefe mit einem Textcompiler wie troff oder TEX, dann sollten die Adressen möglichst automatisch eingefügt werden. In dieser Sprechstunde betrachten wir, wie man mit awk und ein paar anderen Tools sehr schnell zu einer primitiven persönlichen Datenbank kommt. Kurz nachdem man die Manual-Seite grep(1) gelesen hat, richtet man sich vermutlich folgendes Kommando tel zur Ausgabe von Telefonnummern ein: $ cat tel grep —i "${1:—.}"
um/91/6-415 © 1991 Axel-Tobias Schreiner 2 grep dann alle nicht-leeren Zeilen erkennt. Ein Shell-Skript editiert sich selbst Erfährt man eine neue Telefonnummer, dann muß man sich daran erinnern, wo tel abgespeichert ist, damit man im Shell-Skript eine neue Datenzeile einträgt. Bequemer ist es natürlich, wenn tel ohne Argument gleich einen Editor aufruft und uns zum Eintragen auffordert: if [ ! "$1" ] then exec vi +’/ˆend/—’ tel fi In den Klammern [ ] gibt man eine Bedingung für test(1) an, die hier untersucht, ob $1 ein leerer String ist. Trifft dies zu, so wird vi(1) aufgerufen und mit der Option + gleich auf das Ende der Telefonliste positioniert, also auf die Zeile, die vor der mit end beginnenden Zeile steht. Der Editor wird für das Shell-Skript selbst aufgerufen, deshalb verwenden wir exec, damit nach Abschluß des Editierens das dann vermutlich verlängerte Shell- Skript nicht weiter ausgeführt wird — das würde sonst mindestens die Shell zu bissigen Bemerkungen verleiten! Praktischer ist vielleicht noch, wenn der Aufruf $ tel + Axel 0541 969 2480 selbst den neuen Eintrag in die Datenzeilen des Skripts vornimmt: if [ "$1" = ’+’ ] then shift sed ’/ˆend/i\
um/91/6-415 © 1991 Axel-Tobias Schreiner 3 ’"$*" $0 > .$0 chmod +x .$0 exec mv .$0 $0 fi Diesmal ist die Bedingung, daß als erstes Argument ein einzelnes + angegeben wird. shift entfernt das Argument und sed(1) fügt den Rest der Kommandozeile unmittelbar vor der end-Zeile in unser Shell-Skript $0. Das neue Shell-Skript .$0 entsteht als Ausgabe von sed; es wird mit chmod(1) ausführbar gemacht und dann mit mv an die Stelle des ursprünglichen Skripts gebracht. exec ist hier nur effizienter aber nicht unbedingt nötig: die Shell hat auch dann noch Zugriff auf das alte Shell-Skript, wenn sein Name per mv verschwindet. Ein Problem ist bei der Installation natürlich zu beachten: an Stelle von $0 müssen voll qualifizierte Namen eingefügt werden. Beim Aufruf wird tel ja vermutlich auf unserem PATH gefunden, und wir wollen tel an der Fundstelle und nicht etwa tel in Form von $0 in irgendeinem anderen Katalog ändern. Diese Beispiele sind ohnehin nur sehr private Telefonbüchlein: tel ist nur für den praktisch, der das Shell-Skript ändern darf, und der möglicherweise sogar Schreibzugriff zu dem Katalog braucht, in dem das Skript gespeichert ist. Existiert ein Link auf das Skript, so wird er durch mv auch noch zerstört! Adrema für Anfänger In einer früheren Sprechstunde wurde vorgeführt, wie man troff und ein Makropaket wie ms zum Schreiben von Briefen umfunktioniert. Der Trick bestand darin, wenigstens einen Makro .Ad zum Abspeichern der Adresse und einen Makro .Br zum Aufbau eines Briefkopfs einzuführen. Von diesen Makros abgesehen, kann man das Makropaket ziemlich unverändert verwenden. .Br sorgt dafür, daß die Adresse
um/91/6-415 © 1991 Axel-Tobias Schreiner 4 im Umschlagfenster zentriert erscheint, und positioniert außerdem so, daß unmittelbar anschließend die Anrede erfolgen kann. Ein typischer Brief beginnt dann folgendermaßen: .Um \" waehlt Briefkopf .Ad Herrn H. J. Niclas Carl Hanser Verlag Postfach 86 04 20 8000 München 86 .Br Sehr geehrter Herr Niclas, .PP ich habe wieder zu viele Artikel für die unix/mail! Auf die Dauer wird das Tippen einer häufig verwendeten Adresse in jedem Brief natürlich recht lästig. In erster Näherung packt man die Adresse, den Aufruf von .Br und die Anrede in eine Datei mit (Nach-)Namen Niclas, und der Briefanfang reduziert sich auf .Um .Ad Niclas In der Definition von .Ad steht dazu am Schluß ungefähr folgende Zeile: .if \\n(.$ .so /usr/lib/address/\\$1 Die Sache klappt, wenn man den Dateinamen als Makro-Argument richtig buchstabiert, und wenn man für neue Dateien mit Adressen Schreibzugriff auf den Katalog hat.
um/91/6-415 © 1991 Axel-Tobias Schreiner 5 Formular für vi Auch hier empfiehlt es sich, zum Abfragen oder Eintragen von Adressen ein Kommando address einzuführen, denn dann muß man sich wieder den Pfad zum Adreßverzeichnis nicht merken: cd /usr/lib/address if [ ! "$1" ] then vi +’f newname’ template else cat "$1" fi Wir wechseln zunächst zum Adreßverzeichnis, denn dann kann man sich vom Editor aus sehr leicht ähnliche Adressen ansehen. Wird wirklich eine neue Adresse hinterlegt, holen wir in den Editor eine ‘‘typische’’ Adresse aus der Datei template, die als eine Art Formular dienen kann. template ist natürlich schreibgeschützt, damit das Formular nicht versehentlich überschrieben wird. Als noch deutlicheren Hinweis hinterlegen wir mit dem f- Kommando des Editors den Namen newname einer ebenfalls schreibgeschützten Datei. Dieser Dateiname taucht zum Beispiel dann auf, wenn man versucht, den neuen Adreßtext abzuspeichern. Wir könnten auch wieder + als Argument für address einführen und dann bei vi gleich den richtigen Dateinamen hinterlegen.
um/91/6-415 © 1991 Axel-Tobias Schreiner 6 Information sharing Die Datensammlungen für tel und address wachsen nun eine Zeitlang still vor sich hin. Da man die Adressen vermutlich von Visitenkarten abtippt, dürften sich bald in den Adreßdateien als troff-Kommentare auch Telefonnummern sammeln, die man ‘‘später’’ noch zu tel hinzufügen will. Es geht auch mit brachialer Gewalt: a=`grep —i —l "$1" \ /usr/lib/address/*` if [ "$a" ] then cat $a fi Mit grep werden am Schluß von tel einfach auch noch die Adreßdateien durchsucht; die Option −l liefert die Namen der Dateien mit dem gesuchten Inhalt als Resultat. Damit cat nicht etwa ohne Argumente aufgerufen wird und auf die Standard-Eingabe wartet, untersuchen wir zuerst, ob überhaupt ein Dateiname gefunden wurde, und geben erst dann alle gefundenen Dateien aus. Wenn Sie einen großen Bekanntenkreis Ihrem treuen UNIX-Gefährten anvertrauen, geht diese Methode nicht lange gut: der Adressenkatalog ist ein längerer Pfad, und die Argumentliste für grep dürfte deshalb recht bald überlaufen. Außerdem ist das Durchsuchen vieler einzelner Dateien mit grep für jede Anfrage nach einer Telefonnummer für den Zuschauer ziemlich langweilig.
um/91/6-415 © 1991 Axel-Tobias Schreiner 7 Eine kleine Datenbank Das Experiment mit grep und cat deutet in die richtige Richtung: alle Daten sollten in einer einzigen Datei stehen, die grep schnell durchsuchen kann. Dabei darf ein Eintrag in unserer Datenbank allerdings nur eine einzige Zeile umfassen, denn grep gibt gerade die getroffenen Zeilen aus. Name, Adresse, Postfach, Ort, Telefonnummer, Firma und alles andere auf einer einzigen Zeile unterbringen zu müssen, bedeutet für neue Einträge eine gewisse Zumutung. Wir hätten dann außerdem bei troff gewisse Schwierigkeiten, aus einer Datenbankzeile wieder eine vernünftige Adresse zu gewinnen — schließlich kann ein Programm den Feldern einer solchen Zeile kaum noch ansehen, welchen Teil der Information sie bilden. Eine praktischere Lösung besteht wohl darin, die Informationen als Gruppe von Zeilen in eine Datei zu schreiben, und die Datenbankzeilen für grep erst aus dieser Datei herzustellen. Damit man später die Einzelteile noch erkennen kann, etikettiert man die Informationszeilen und übernimmt die Etiketten in die Felder einer Datenbankzeile. Aus einer Eingabe wie # meine Adressen herr Axel Schreiner org Universität pob 4469 ort 4500 Osnabrück tel 0541 969 2480 fax 0541 969 2770 herr H.J. Niclas firma Carl Hanser Verlag ad Kolbergerstr. 22
um/91/6-415 © 1991 Axel-Tobias Schreiner 8 ort 8000 München 86 generiert ein Kommando build für jede Gruppe von Informationszeilen eine Datenbankzeile, in der die Informationen durch tab getrennt werden, und in der vor jeder Information ihr Etikett steht. Da offensichtlich # in der ursprünglichen Textdatei als Kommentarzeichen verwendet wurde, kann man in der Datenbankzeile Etikett und Information gerade durch # trennen. Umgibt man schließlich jede Datenzeile noch mit je einem tab am Anfang und am Schluß, so ersetzt tr ’#\11’ ’\11\12’ die Trennzeichen # durch tab und tab durch Zeilentrenner, das heißt, aus den Datenzeilen entstehen in etwa wieder die ursprünglichen Textzeilen — nur die Kommentare sind nicht mehr vorhanden und wir erhalten wahrscheinlich einige unnötige Leerzeilen. So einfach ist ein unbuild! Eine Rohfassung von build ist nicht entscheidend komplizierter: { sed ’ /ˆ#/d s/[ ]*#.*$// /ˆ$/s/ˆ/#/’ | tr ’\12\11#’ ’\11#\12’ echo } | sed ’ /ˆ *$/d s/ˆ */ / s/ *$/ /’ Der erste Aufruf von sed löscht Kommentarzeilen mit d, entfernt Zwischenraum und Kommentare am Schluß von Zeilen mit s und schreibt auf jede leere Zeile das Zeichen #. Anschließend ersetzt tr Zeilentrenner, tab und #, so daß das gewünschte Dateiformat entsteht. echo sorgt für einen abschließenden Zeilentrenner, damit die
um/91/6-415 © 1991 Axel-Tobias Schreiner 9 letzte Zeile später nicht untergeht. Der zweite Aufruf von sed entfernt überzählige leere Zeilen und korrigiert die Anzahl der tabs am Zeilenanfang und Ende. Damit kann aber unsere Datenbank schon ihre Pforten eröffnen: cat textdatei ... | build | grep muster | unbuild sucht nach dem muster in jeder Gruppe von Zeilen in jeder textdatei und gibt die gefundenen Gruppen wieder zeilenweise aus. Mit Hilfe der Etiketten könnte man die Ausgabe noch verfeinern. build An build kann man noch gewisse Ansprüche stellen: • Ordnet man die Etiketten in der Datenbank immer in der gleichen Reihenfolge an, kann man mit grep eleganter zum Beispiel nach Kombinationen von Name und Firma oder Ort suchen. • Da man mit den Etiketten später wohl verschiedene Ausgaben bei tel und address und gar in einem troff-Brief produzieren wird, sollte man die möglichen Etiketten vorab definieren und Buchstabierfehler abweisen. • Eine bestimmte Menge von Etiketten, zum Beispiel Name, Firma und Organisation, sollten einen Eintrag in der Datenbank eindeutig bestimmen; build muß dann Duplikate abweisen. • Hat man genügend Platz und wenig Zeit, sollte build die Datenbank nur erzeugen, wenn sich die zugehörigen Textdateien im Adreßkatalog geändert haben. Den letzten Punkt kann man leicht mit der Shell und find(1) verwirklichen: wir nehmen an, daß build als einziges Argument den Namen eines Katalogs erhält, in dem die Textdateien für die Datenbank stehen. Die Datenbank legen wir dann am
um/91/6-415 © 1991 Axel-Tobias Schreiner 10 besten als ‘‘verborgene’’ Datei in diesem Katalog an: db=.database build kann bei Bedarf dann alle durch * erreichbaren Dateien bearbeiten, denn * trifft den verborgenen Dateinamen nicht. Ob es für build wirklich etwas zu tun gibt, entscheidet der in Abbildung 1 gezeigte Teil des Shell-Skripts. if [ —f $1/$db ] then if [ ! "`find $1 —newer $1/$db —print`" ] then exit 0 fi fi echo >&2 "$0: $1: building database, please wait..." Abbildung 1: ist ein ‘‘build’’ erforderlich? Wenn die Datenbank existiert, liefert find die Namen aller Dateien im Katalog, die neuer sind als die Datenbank. Ist dies ein leerer String, so wird das build-Skript mit exit verlassen. include für Shell-Skripte Auf die eigentliche Datenbank werden außer build auch noch andere Kommandos zugreifen. Wir haben die Datenbank schon als Shell-Variable db definiert, aber wir sollten auch dafür sorgen, daß alle beteiligten Shell-Skripte den gleichen Wert verwenden. Eine elegante Lösung besteht darin, alle wichtigen Konstanten für diese Shell-Skripte in einer einzigen Konfigurationsdatei config zusammenzufassen und diese Datei dann in jedem Shell-Skript einzufügen, ganz analog zu den
um/91/6-415 © 1991 Axel-Tobias Schreiner 11 Definitionsdateien in C. Die Zuweisung an db steht also in config, und am Anfang von build schreiben wir . config In der Bourne-Shell sorgt ein Punkt als Kommando dafür, daß die als Argument angegebene Datei von der gleichen Shell an dieser Stelle ausgeführt wird, daß also zum Beispiel Variablen importiert werden. Der Punkt entspricht dem #include von C. Natürlich wird man bei der Installation der Kommandos config in einem Katalog wie /usr/lib/db verstecken — dann muß man in jedem beteiligten Shell-Skript bei der Installation an Stelle von config den vollständigen Pfad einfügen. Die Pflege der Kommandos wird dadurch aber deutlich erleichtert. Textverarbeitung Die eigentliche Arbeit in build leisten wir mit Hilfe von awk — schließlich wollen wir die Etiketten überprüfen, Duplikate erkennen, usw. Beim ‘‘neuen’’ awk, der zum Beispiel mit System V Release 4 ausgeliefert wird, kann man eigene Funktionen definieren. Damit ist die Hauptschleife von build kein Problem: /ˆ#/ { next } Beginnt eine Zeile mit #, so wird sie vollständig ignoriert, denn next verlangt, daß awk zum Anfang seiner Mustertabelle springt und die nächste Eingabezeile bearbeitet. /#/ { $0 = substr($0, 1, index($0, "#") — 1)
um/91/6-415 © 1991 Axel-Tobias Schreiner 12 } Enthält eine Zeile das Kommentarzeichen #, so wird sie auf den Teil vor dem Kommentarzeichen verkürzt. Bei awk darf man an Eingabefelder wie $1 und beim neuen awk sogar an die ganze Eingabezeile $0 zuweisen. Bei dieser Implementierung von build verschwinden Kommentarzeilen vollständig. Steht aber vor dem Kommentar noch Zwischenraum, dann wirkt die Zeile im Folgenden als Leerzeile, sie trennt also Gruppen von Informationszeilen. Das mag verblüffen, aber ich glaube, daß es so dem optischen Eindruck am besten entspricht. Man könnte next durch $0 = "" ersetzen und damit auch Kommentarzeilen als leere Zeilen behandeln. /ˆ[ ]*$/ { output() next } Eine leere Zeile beendet eine Gruppe von Informationszeilen, und wir müssen eine Zeile für die Datenbank ausgeben. Folgen mehrere leere Zeilen nacheinander, darf output() nur eine einzige Ausgabezeile konstruieren. Man vergißt leicht, daß eine Gruppe von Informationszeilen auch durch das Ende einer Datei abgeschlossen werden kann. build soll für viele Dateien funktionieren: FILENAME != filename { filename = FILENAME output() } END { output() } Eine Datei ist beendet, wenn sich FILENAME ändert, also der Name der aktuellen Eingabedatei, oder wenn awk ganz am Schluß die END-Bedingung abarbeitet. Mit
um/91/6-415 © 1991 Axel-Tobias Schreiner 13 Funktionen kann man Code-Verdopplung in awk-Programmen vermeiden. Von der Shell zu awk Bei der Eingabe wollen wir die Schreibweise der Etiketten kontrollieren. config weist dazu an die Shell-Variable tags eine Liste möglicher Etiketten zu: tags=’[hH]err|[fF]rau .irma’ Im Ernstfall sind das vermutlich mehr, aber die Pointe besteht darin, Muster als Etiketten zu erlauben, denn dann kann man zum Beispiel Unterschiede zwischen Groß- und Kleinschreibung ignorieren lassen. Die Folge der Muster in $tags soll außerdem die Anordnung der Felder in einer Zeile der Datenbank definieren — hier ermöglichen die Muster, daß ein Feld wahlweise für verschiedene Etiketten genutzt werden kann. Wir wollen den Wert der Shell-Variablen tags als Teil eines Arguments von awk einfügen. Da dieser Wert Zwischenraum enthält, und da nur ein einziges Argument entstehen darf, benötigen wir in der Shell "$tags" Der Wert soll Teil des awk-Programms werden, das in einfachen Anführungszeichen übergeben wird. Wir müssen die anderen Programmteile und unseren Variablenwert aneinanderfügen: $ awk ’ ... ’"$tags"’ ... ’ Schließlich muß der Variablenwert im awk-Programm noch zu einem String werden. Dafür benötigen wir Anführungszeichen im Programm: $ awk ’ ... "’"$tags"’" ... ’
um/91/6-415 © 1991 Axel-Tobias Schreiner 14 Will man an Stelle von $tags die Ausgabe eines Kommandos wie date in das awk- Programm hineinflicken, werden auf engstem Raum wirklich alle drei Arten von Anführungszeichen verwendet! Variable Muster Ein Etikett, also bei der späteren Eingabe das Feld $1, muß sich mit der Etikettliste vertragen. Das ist dann leicht zu prüfen, wenn wir $1 mit einzelnen Mustern aus der Liste vergleichen können. awk ’ BEGIN { ntag = split("’"$tags"’", tag, " ") split() zerlegt einen String an den Textmustern, die als drittes Argument angegeben werden, hier also an Leerzeichen. Die einzelnen ‘‘Worte’’, hier also die einzelnen Etikettmuster aus $tags, werden dann als Elemente eines awk-Vektors abgelegt, der von 1 bis zum Resultatwert von split() indiziert wird. Hier ist das der Vektor tag[], das zweite Argument für split(). Gehen wir einmal davon aus, daß die meisten Etiketten in $tags als einfache Worte angegeben werden, dann sollten wir vermeiden, daß wir für jede Eingabezeile tag[] mit einer expliziten Schleife absuchen müssen. Wir führen deshalb einen zweiten Vektor pos[] ein, bei dem wir die Etikettmuster als Indizes verwenden. Zu jedem Muster enthält pos[] seine Position in der Etikettliste als Wert. for (n = 1; n
um/91/6-415 © 1991 Axel-Tobias Schreiner 15 $1 in pos { input(pos[$1], $1, $2) next } War in $tags wirklich ein Etikettmuster angegeben, müssen wir noch einzeln vergleichen: { for (n in pos) { if ($1 !˜ n) continue input(pos[n], $1, $2) next } fatal("bad input") } Hier machen wir uns einen der interessanteren Aspekte des neuen awk zunutze: n durchläuft die Indexwerte von pos[], also die einzelnen Etikettmuster aus $tags. In der if-Anweisung wird untersucht, ob das Eingabe-Etikett $1 dem Muster in n genügt. Im neuen awk kann man Variablenwerte als Muster betrachten, zum Beispiel indem man sie als rechte Operanden der Muster-Operatoren ˜ und !˜ angibt. if ($1 !˜ n) untersucht also, ob das Eingabe-Etikett $1 zu einem der Muster n aus der Etikettliste paßt, die alle in pos[] gespeichert sind.
um/91/6-415 © 1991 Axel-Tobias Schreiner 16 Funktionen Die Hauptschleife von build ist jetzt so ziemlich fertig. Wir müssen jetzt nur noch die Funktionen input(), output() und fatal() implementieren. In input() sammeln wir Etiketten und Informationen in zwei Vektoren tags[] und items[]: function input (p, t, i) { items[p, ++ item[p]] = i tags[p, item[p]] = t new = 1 } Wir erlauben, daß Etiketten in einer Gruppe von Eingabezeilen mehrfach verwendet werden, deshalb benützen wir zwei Indizes: die Position p in der Etikettliste und einen Zähler item[p] je Etikett. Beim neuen awk kann man mehrere Vektor-Indizes mit Komma trennen; der eigentliche Index entsteht durch Verkettung mit dem Wert von SUBSEP. function output ( p, i) { if (! new) return unique() for (p = 1; p
um/91/6-415 © 1991 Axel-Tobias Schreiner 17 Abbildung 2: Ausgabe einer Datenzeile Abbildung 2 zeigt die Funktion output(). Sie erhält zwar keine Argumente, aber wir verwenden Parameter an Stelle der in awk nicht vorhandenen lokalen Variablen. Mit p durchlaufen wir die Positionen der Etikettliste, und für jede Position geben wir alle gespeicherten Etiketten und Informationen aus. new überwacht zwischen input() und output(), daß jede Datenzeile nur einmal ausgegeben wird. Datenzeilen sollen in bezug auf die Information für eine bestimmte Gruppe von Etiketten eindeutig sein. Diese Etiketten übernehmen wir genau wie tags aus der Datei config und legen sie mit split() in einem Vektor key[] ab. In der Funktion unique() (Abbildung 3) verketten wir dann alle Informationen für jedes derartige Etikett. function unique ( k, n, f, i) { k = "" for (n = 1; n
um/91/6-415 © 1991 Axel-Tobias Schreiner 18 Zur Fehlersuche bei den vielen Vektoren ist eine Funktion dump() ganz praktisch: function dump (vec, n) { for (n in vec) print n, vec[n] } Vektoren werden nur als änderbare Argumente übergeben. dump() gibt alle Elemente eines beliebigen Vektors aus. Fehlermeldungen Damit bleibt nur noch die Funktion fatal() übrig, die eine Fehlermeldung zur Diagnose-Ausgabe schreiben soll (Abbildung 4). function fatal (message) { printf "%s:", "’"$0"’" | stderr if (FILENAME != "—") printf "%s", FILENAME | stderr if (FNR) printf "(%g)", FNR | stderr if (length) printf " in \"%s\"", $0 | stderr printf " %s\n", message | stderr exit 1 } Abbildung 4: Fehlermeldung in ‘‘awk’’ Vor der eigentlichen Meldung geben wir als Kontext einige Informationen aus: den Programmnamen aus der Shell-Variablen $0; den aktuellen Dateinamen aus der awk-Variablen FILENAME, wenn nicht die Standard-Eingabe bearbeitet wird; die im
um/91/6-415 © 1991 Axel-Tobias Schreiner 19 neuen awk verfügbare Zeilennummer FNR in der aktuellen Datei und die Eingabezeile $0, falls sie nicht die Länge 0 besitzt. fatal() akzeptiert zwar nur ein Argument, aber man sieht zum Beispiel beim Aufruf in unique(), daß das eine Verkettung mehrerer Strings sein kann. Alle Ausgaben sollen zur Diagnose-Ausgabe gehen, deshalb definieren wir vor dem ersten Aufruf von fatal() BEGIN { stderr = "cat >&2" } Wir benötigen zwar cat als zusätzlichen Prozeß, aber wir erreichen wirklich die Diagnose-Ausgabe, die dann für das Shell-Skript wieder umgelenkt werden kann. Will man unbedingt das Terminal erreichen, würde man bei printf statt der Pipeline zu cat folgende Ausgabe-Umlenkung angeben: printf ... > "/dev/tty" So kann aber eine Fehlermeldung nicht mehr umgelenkt werden. query Die Formatierung der Abfrage-Resultate mit tr ist sicher sehr elegant, aber man kommt leicht auf weitere Ideen: • Der Sinn der Etiketten war, Information später wieder wählen zu können; bei query sollte man also eine Liste von Etiketten angeben dürfen. • Arbeitet man mit anderen zusammen, kann es gemeinsame Adressen für alle und daneben noch private Sammlungen geben; findet man die Information in der eigenen Sammlung, dürfte sich ein Zugriff auf die allgemeinen Informationen erübrigen.
um/91/6-415 © 1991 Axel-Tobias Schreiner 20 • Die Suche muß man wohl steuern können: manche Benutzer sind an allgemeinen Informationen nicht interessiert und setzen eine ‘‘lokale’’ Option −l, andere möchten die globalen Informationen auf alle Fälle erhalten und setzen −g. Betrachten wir zunächst die Steuerung der Suche. Die Shell-Variable mode soll die Option enthalten, und in config haben wir einen globalen Katalog in glob und einen lokalen Katalog in priv hinterlegt: glob=/usr/lib/address priv="$ADDRESS" So wird der lokale Katalog einer Environment-Variablen entnommen, die jeder Benutzer selbst setzen kann. Die Suche gestaltet sich nun folgendermaßen: for dir in "$priv" $glob do if [ ! "$dir" ] then stat=1 else build $dir grep ... $dir/$db | tr ’#\11’ ’\11\12’ | awk ... stat=$? fi case "$mode" in —l) exit $stat ;; —g) ;; *) if [ "$stat" != 1 ] then exit $stat fi esac done exit $stat
um/91/6-415 © 1991 Axel-Tobias Schreiner 21 dir durchläuft die Kataloge und stat enthält den Erfolg der Suche, also normalerweise den Exit-Code von awk, den wir im Programm mit exit setzen können. Die case-Anweisung steuert, ob die Suche auf alle Fälle nach dem lokalen Katalog endet, ob sie unbedingt über alle Kataloge geht, oder ob sie nach einem Erfolg von awk endet. Ein Fehler im awk-Programm kann dabei mit exit 2 an stat gemeldet werden, ein Erfolg mit 0. Das awk-Programm von query unterscheidet sich praktisch nur in der Formatierung der Ausgabe, also in output(), vom build-Programm, denn wir sorgen mit der früher besprochenen unbuild-Technik dafür, daß die Eingabe zu awk in beiden Fällen praktisch gleich aussieht. queryf Ein Kommando wie address kann man sicher gut auf query abbilden. Bei tel ist es aber wahrscheinlich lästig, wenn die Antwort pro Telefonnummer mehr als eine Zeile umfaßt. Eine sehr flexible Lösung besteht darin, daß man einfach ein printf-Format mit Informationen füllen darf, die durch die Etiketten ausgewählt werden. tel funktioniert dann vielleicht so: queryf "%s, %s, %s\n" \ ’herr|frau firma tel’ Die erste Zeichenkette enthält die Information mit Etikett herr oder frau, dann kommt die Firma und zum Schluß die Telefonnummer. Die Ausgabe sieht besser aus, wenn man bei fehlender Information das Formatelement samt folgendem Text (wie hier Komma und Zwischenraum) ausläßt. Damit dann der abschließende Zeilentrenner nicht verlorengeht, führt man ein Formatelement %& (in Anlehnung an troffs \&) ein, das keine Ausgabe erzeugt, aber
um/91/6-415 © 1991 Axel-Tobias Schreiner 22 eben auch nicht entfernt wird. Man kann auch queryf mit einem Schalter −n dazu überreden, ohne vollständige Informationen gar nichts auszugeben. queryf ist überraschend leicht zu implementieren, wenn man auf einen Trick zur eleganten Verarbeitung des printf-Formats kommt: mit split() zerlegt man das Format einfach an allen %-Zeichen: nfmt = split("’"$fmt"’", fmt, " ") So wird das Format $fmt aus der Shell in den Vektor fmt[] im awk-Programm übertragen. Jetzt muß man in output() nur noch die einzelnen Elemente von fmt[] bearbeiten: printf "%s", fmt[1] fmt[1] enthält den Teil des Formats vor dem ersten Steuerzeichen. Dieser Teil wird unverändert ausgegeben. i = 1 for (f = 2; f
um/91/6-415 © 1991 Axel-Tobias Schreiner 23 Andernfalls verketten wir vor das Vektorelement einfach ein % und lassen printf die Ausgabe der Information erledigen. Ohne Information überspringen wir das Formatelement. Auch %& ist trivial einzufügen: else if (fmt[f] ˜ /ˆ&/) printf "%s", substr(fmt[f], 2) Die Lösung umgeht das Problem, daß wir das Format direkt nicht verwenden können, weil wir die Anzahl der Vektorelemente nicht kennen, die wir sonst bei printf noch angeben müssen. awk hat kein Äquivalent zu vprintf(3), aber wie man sieht, ist das auch kaum notwendig. mfg queryf eignet sich nicht ganz zum Einflicken von Adressen und Anreden in Briefe. Vor allem die Anrede gehört an die richtige Stelle in der troff-Eingabe, und für die Adresse wird man lieber ein Postfach als eine Straßenadresse angeben. Das queryf- Format ist so schön leicht zu merken, daß man es besser nicht mit Bedingungen als Formatelementen überädt. mfg ist ein einfacher troff-Preprozessor, der entweder von seiner Kommandozeile oder von einem .Ad-Aufruf ein Suchmuster abholt, query nach den nötigen Daten fragt, und bei .Br dann eine geeignete Adresse und Anrede einfügt. Die Schwierigkeit in der Implementierung von mfg besteht darin, daß das Suchmuster ein Argument des Makros .Ad sein darf, den wir erst beim Kopieren der troff-Quelle entdecken. Die Hauptschleife von mfg ist eigentlich offensichtlich: BEGIN { pattern="’"$pattern"’"
um/91/6-415 © 1991 Axel-Tobias Schreiner 24 } /ˆ\.Ad/ { if (pattern == "") pattern = $2 next } /ˆ\.Br/ { ... } { print } Bleibt nur die Frage, wie man vor .Br die richtige Adresse und danach eventuell eine Anrede ausgibt. Das Problem besteht darin, daß man hier in etwa den Algorithmus von query einfügen müßte, das heißt, das awk temporär einen anderen Datenstrom verarbeiten sollte. Es geht aber viel einfacher — schließlich weiß ja query, wie man eine entsprechende Anfrage beantwortet. Im neuen awk kann man mit getline aus einer Datei oder Pipeline lesen. if (pattern != "") { query = "’"$query $mode \’"’" pattern "’\’’" while (query | getline line) if (split(line, field, "\t") == 2) info[field[1]] = info[field[1]] "\n" field[2] print ".Ad" zHdv = "" if (put("", "firma")) zHdv = "z.Hd.v. " put(zHdv "Frau ", "frau") put(zHdv "Herrn ", "herr") if (! put("Postfach ", "pob")) put("", "ad")
um/91/6-415 © 1991 Axel-Tobias Schreiner 25 put("", "ort") } Abbildung 5: Konstruktion einer Adresse in ‘‘mfg’’ Abbildung 5 zeigt, daß man zunächst in query ein Shell-Kommando zusammensetzt, das query aufruft; dabei wird das Suchmuster pattern durch einfache Anführungszeichen geschützt, was bei awk einen gewissen Aufruhr an Anführungszeichen verursacht. Anschließend liest getline aus der Pipeline in query jeweils eine Zeile in die Variable line ein. Diese Zeile wird dann mit split() zerpflückt und in info[] abgelegt. getline liefert 0 am Schluß seiner Eingabe. Der Rest ist sehr simple Textverarbeitung. Eine Funktion put() gibt gegebenenfalls einen Präfix und dann ein Element von info[] aus: function put (pre, tag) { if (! (tag in info)) return 0 print pre \ substr(info[tag], 2) return 1 } Je nach Erfolg wird die Adresse aus Straße oder Postfach zusammengesetzt. Die Quellen Über den neuen awk informiert man sich am besten in Aho, Kernighan und Weinbergers Buch The awk programming language oder auch in den Manualseiten von System V oder in der ausführlichen Beschreibung des GNU awk, der ebenfalls die hier verwendeten Möglichkeiten bietet. Die beste Informationsquelle zur Shell-
um/91/6-415 © 1991 Axel-Tobias Schreiner 26 Programmierung ist nach wie vor Kernighan und Pikes Der UNIX Werkzeugkasten. Briefe wurden erstmals in der unix/mail Sprechstunde 1/86 vorgeführt und im Sprechstunde-Buch [Hanser 1987] verbessert. Adressen habe ich automatisiert, als ich die Herausgabe dieser Zeitschrift übernahm und damit zu einem relativ ‘‘regelmäßigen’’ Kundenkreis für Briefe kam. Inzwischen verwenden wir die Brief- Makros auch an der Universität, und daraus erwuchs die Notwendigkeit, private Daten halten zu können. Prinzipiell könnte man refer, oder Tim Budds bib, von Literaturverweisen sicher auch auf Adressen umfunktionieren, aber dazu ist ein relativ hoher Aufwand nötig, um die Datenbasis zu unterhalten und korrekte Adreßformate zu erzielen. Man erhält dafür natürlich wesentlich effizientere Suchverfahren, aber eine private Adreßliste lohnt den Aufwand vermutlich nicht. Einfache Kommandos wie tel oder address sind mit refer oder bib auch nicht leicht zu realisieren. Die hier skizzierten Shell-Skripte sind etwas zu lang, als daß man sie ganz abdrucken sollte. Interessenten stehen die Quellen (inklusive Brief-Makros und GNU awk) gerne zur Verfügung: für einen Unkostenbeitrag von DM 25.00 können sie als 5 1/4" Diskette im MS/DOS Format bei Axel Schreiner im Fachbereich 6 an der Universität in 45 Osnabrück bezogen werden.
Sie können auch lesen