Früh übt sich - das Telefonbuch - Axel-Tobias Schreiner,Universität Osnabrück

Die Seite wird erstellt Eva Keßler
 
WEITER LESEN
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