Dialogregie

Teil 7: Benutzerfreundliche Ein-/Ausgabe

01.02.2002 Im letzten Teil des Bash-Programmierkurses geht es um die benutzerfreundliche Gestaltung von Eingaben und Dialogen am Beispiel der Programme dialog, gdialog und kdialog.

Nach langer Pause haben Sie nun den siebten Teil des Bash-Kurses in der Programming Corner vorliegen. Zum Abschluss beschäftigen wir uns mit den Möglichkeiten, Eingaben vom Benutzer abzufragen oder Ausgaben übersichtlich auf den Bildschirm zu bringen. Zudem werden Sie viele Problemstellungen aus den bisherigen Folgen wieder finden, die noch einmal wiederholt und in einem größeren Zusammenhang angewendet werden. Die Teile 1-6 der Programming Corner finden Sie vollständig auf der Heft-CD.

Dieser letzte Teil der Bash-Programmierung ist aber gleichzeitig der erste Teil der neuen Programming Corner: Ab jetzt werden wir uns in loser Folge mit dem Zusammenspiel der Bash mit externen Programmen beschäftigen und auch andere Programmiersprachen vorstellen.

Das Abschlussprojekt ist ein Terminwarner, der zum Beispiel zur Geburtstagserinnerung eingesetzt werden kann. Nach Einrichten des Server-Teils mittels Cron-Job erhält man tägliche Vorwarn-Mails, bis der Termin verstrichen oder gelöscht ist.

Zur Installation des Terminwarners kopieren Sie die beiden Skripte termin-client und termin-server von der Heft-CD nach /usr/local/bin. Zudem müssen Sie dialog, kdialog oder gdialog installiert haben. Wir empfehlen derzeit gdialog von Gnome, das grafische Frontend lässt sich damit am einfachsten bedienen. Wer ansonsten keine Gnome-Programme verwendet sollte bedenken, dass gdialog aus dem Paket gnome-utils einige Gnome-Bibliotheken benötigt, die gegebenenfalls zusätzlich nachinstalliert werden müssen.

Aufbau der Terminliste

Die eingegebenen Termine werden in der Datei ~/.termine im jeweiligen Heimatverzeichnis des Benutzers gespeichert. In der ersten Zeile steht die Mail-Adresse, an die Terminwarnungen verschickt werden sollen, in allen weiteren steht ein Termin pro Zeile. Jede Terminzeile hat fünf Felder, die durch Tabulatoren voneinander getrennt sind: Datum, Vorwarnzeit in Tagen, Betreff, Beschreibung und zusätzliche Mail-Adressen (CC-Adressen). Einzige Besonderheit: Das Datum wird im Format Jahr, Monat, Tag ohne Leer- oder Trennzeichen gespeichert; Weihnachten wäre in dieser Darstellung 20021224. In dieser Schreibweise braucht man die Terminzeilen lediglich numerisch sortieren zu lassen, um auch eine Sortierung in aufsteigender Datumsfolge zu erreichen.

Termin-Server einrichten

Das Programm termin-server kündigt die Termine rechtzeitig per Mail an, wenn es täglich aufgerufen wird - am Besten durch einen Cron-Job des Benutzers. Mit dem Kommando crontab können Sie einen solchen Job als normaler Benutzer eintragen. Vorher sollten Sie mittels EDITOR-Variable den von Ihnen bevorzugten Editor angeben:

export EDITOR="kedit"
crontab -e

Im Editor tragen Sie nun die Befehlszeile für Cron ein. Das folgende Beispiel zeigt einen Aufruf täglich um 12 Uhr mittags. Da der Rechner zu diesem Zeitpunkt laufen muss, damit der Cron-Job überhaupt abgearbeitet wird, sollten Sie die Uhrzeit an Ihre Gewohnheiten anpassen:

0 12 @L: * @L: * @L: *     /usr/local/bin/termin-server

Server im Detail

In Listing 1 finden Sie den termin-server aufgeschlüsselt. Aus Platzgründen haben wir die Kommentare, die in den Skripten auf der CD selbstverständlich enthalten sind, für den Abdruck entfernt und die Zeilen durchnumeriert.

Der Mail-Versand an den Benutzer erfolgt in der Funktion TerminWarnung von Zeile 3 bis 12. Die Funktion erwartet sechs Parameter. Als erstes muss die Mail-Adresse des Empfängers und dann alle fünf aufgeschlüsselten Felder der betreffenden Terminzeile übergeben werden. Der Versand erfolgt in Zeile 9 bis 11 über das Programm mail, ein rudimentäres Mail-Programm, das sich hervorragend über Skripte steuern lässt. Sind beim Termin zusätzliche Mail-Adressen angegeben, stehen diese in $6 und müssen in der Form -c Adresse,Adresse,... als Parameter an mail übergeben werden. Dementsprechend wird in Zeile 6 der Parameter für mail aufgebaut und in Zeile 9 gleich als erstes angehängt.

Die Zeilen 9 bis 11 enthalten noch weitere Besonderheiten: So wird das Datum, bei Übergabe in $2 gespeichert, mittels date-Aufruf in die in Deutschland übliche Schreibweise mit Wochentag, Tag, Monat und Jahr gebracht. Die ausführliche Beschreibung des Termins wird über den Eingabeblock "<< EOF … EOF" an mail weitergereicht. Prinzipiell ginge dies auch per echo und Pipe, beim Eingabeblock brauchen wir uns aber nicht um das Escapen von Anführungszeichen oder Trennzeichen zu kümmern.

IFS zum Ersten

Bei Aufruf von termin-server wird die Funktion zunächst übersprungen, der erste Befehl steht in Zeile 14: Falls keine Terminliste existiert oder sie nicht lesbar ist, wird die Fehlermeldung aus Zeile 15 automatisch von Cron per Mail an den User verschickt.

In den Zeilen 18 bis 22 wird die Terminliste eingelesen, wobei die oft verkannte Variable IFS eine entscheidende Rolle spielt: Mit dieser Variablen wird festgelegt, welche Zeichen die Bash als Trennung von Zeilen, Elementen oder Parametern interpretiert. Standardmäßig sind das Leerzeichen, Tabulatoren ('\t') und Zeilenumbrüche ('\n'). Dies gilt auch für das Einlesen von Arrays per read - Leerzeichen und Tabulatoren werden als Trenner der einzelnen Elemente der Zeile und Zeilenumbrüche als Abschluss einer Zeile interpretiert.

Unsere Terminliste soll zunächst komplett eingelesen und zeilenweise in Arrays gespeichert werden. Da read nur bis zum ersten Zeilenumbruch arbeitet, müssen wir die Umbrüche ersetzen - in unserem Fall durch Returns ('\r'). Dies geschieht in Zeile 21, wobei durch das grep-Kommando zusätzlich noch Kommentare mit Raute am Anfang einer Zeile entfernt werden. Damit liest read zwar die ganze Datei, jedoch zunächst in eine einzelne Variable. Um jede Terminzeile in eine eigene Variable eines Arrays gespeichert zu bekommen, verwenden wir read -a, und setzen kurzerhand die Trenner-Variable IFS auf '\r'.

… und zum Zweiten

In Zeile 24 wird die Mail-Adresse des Benutzers gewonnen - sie steht in der ersten Zeile der Terminliste und bedarf keiner weiteren Behandlung. Anders die Terminzeilen, diese müssen wir erst noch in ihre Elemente zerlegen. Nach Zerlegung muss anhand von Datum und Vorwarnzeit geprüft werden, ob das aktuelle Datum zwischen Beginn der Vorwarnzeit und dem Termin liegt und gegebenenfalls die Mail verschickt werden. Das alles passiert in den Zeilen 26 bis 39.

Die while-Schleife in Zeile 30 arbeitet der Reihe nach alle Terminzeilen ab. In Zeile 32 erfolgt die Zerlegung der Terminzeile mittels set-Anweisung: die übergebenen Parameter werden in den Parameter-Variablen ($1, $2 usw.) gespeichert. Da unsere Elemente in jeder Terminzeile durch Tabulatoren getrennt sind, wird IFS in Zeile 26 auf Tabulator ('\t') gesetzt.

Vergangenheitsbewältigung

Für den Vergleich, ob das aktuelle Datum zwischen dem Termin und dem Beginn der Vorwarnzeit liegt, müssen wir zunächst das Datum der ersten Vorwarnung berechnen. Für Weihnachten und eine Vorwarnzeit von einer Woche ist das nicht weiter schwierig: Vom Tag des Termins, hier 24, brauchen wir lediglich den Vorwarnzeitraum, also 7 Tage, abzuziehen - heraus kommt, dass wir ab dem 17. des Monats warnen müssen. Wenn wir aber einen Termin am ersten März haben, der zwei Tage vorgewarnt werden soll, wird es komplex: Neben dem Monats-Umsprung müssen wir berechnen, ob wir ein Schaltjahr haben oder nicht.

Viel einfacher ist es, das Programm date für diesen Zweck zu verwenden. date kann Datumsformate in fast beliebiger Eingabeform erkennen und damit rechnen. Für unsere Zwecke ist die umgangssprachliche Formulierung der Datumsangabe interessant: Mit der Formulierung "20021224 7 days ago" berechnet date das Datum, das sieben Tage vor dem 24. Dezember 2002 liegt - unter Berücksichtigung aller Seiteneffekte wie Monatsgrenzen und Schaltjahre. Die Ausgabe erfolgt wieder im gepackten Datumsformat mit Jahr, Monat und Tag ohne Trennzeichen.

Der Verzicht auf Trennzeichen im gepackten Datumsformat ist begründet: So wird aus jedem Datum eine achtstellige Zahl, die sich mit herkömmlichen mathematischen Operatoren vergleichen lässt. Dass es sich hier tatsächlich um ein Datum handelt, brauchen wir beim Vergleich nicht zu berücksichtigen. In Zeile 35 erfolgt schließlich der Aufruf der Funktion TerminWarnung.

Termin-Client

Programmtechnisch deutlich aufwendiger ist der Termin-Client mit über 200 Zeilen Code. Das Listing 2 teilt sich in acht logische Blöcke: Die Anfangs-Definitionen (Zeile 2), fünf Funktionen (Zeile 20 bis 132), Einlesen der Terminliste (Zeile 136) und das Hauptmenü (Zeile 159).

Abbildung 1: Ein Programm, drei Frontends: dialog (oben), gdialog (links unten) und kdialog (rechts unten) werden fast identisch angesteuert, unterscheiden sich aber in Aussehen und Bedienung

In den Zeilen 2 bis 15 wird das zu verwendende Frontend festgelegt: gdialog von Gnome, kdialog von KDE oder das Text- und ncurses-basierte dialog. Letzteres ist das Ursprungs-Tool, das gdialog und kdialog nachbilden, eine ausführliche Beschreibung finden Sie auf Seite 78 des LinuxUser 11/2001 oder unter http://www.linux-user.de/ausgabe/2001/11/078-ootb/dialog-4.html. Leider ist kdialog nicht ganz kompatibel zu dialog: Auswahlen in Menüs liefert das KDE-Pendant über die Standard-Ausgabe (stdout), während dialog und gdialog althergebracht die Standard-Fehlerdatei (stderr) verwenden. Der Unterschied scheint klein, bedeutet in der Programmierung allerdings einigen Umstand, wenn die Auswahl wie üblich per Umleitung in eine Datei geschrieben wird:

dialog … 2>datei
gdialog … 2>datei
kdialog … 1>datei

Unglücklicherweise akzeptiert die Bash keine Variable an Stelle der Umleitung. Einzig mit der eval-Anweisung ist es möglich, die Umleitung flexibel zu gestalten. Deshalb wird in den Zeilen 2 bis 11 zunächst bestimmt, welches der Programme installiert ist und dementsprechend der Programmname und die passende Umleitung in Variablen gespeichert.

Die Funktionen

Die Funktion AuswahlMenue (Zeile 20 bis 59) zeigt, wie der Name schon sagt, ein Auswahl-Menü an. Der erste Parameter ist der Titel des Menüs, im zweiten sind die Tastenkürzel als Tabulator-getrennte Liste und im dritten schließlich die Beschreibung der einzelnen Menüpunkte wiederum mit Tabulatoren getrennt. Die Tastenkürzel aus dem zweiten Parameter werden zudem als Rückgabewert genutzt, wenn ein Menüpunkt ausgewählt wurde.

Wie schon vom Server bekannt werden die beiden Tabulator-getrennten Listen in den Zeilen 26 bis 34 und 36 bis 43 mit Hilfe der IFS aufgetrennt und in Arrays gespeichert. Das ist notwendig, weil alle drei Frontends eine andere Reihenfolge erwarten: Kürzel und Beschreibung stehen dort immer paarweise zusammen. Diese Reihenfolge wird in Zeile 45 bis 48 in der Variable Menu generiert.

Die Overhead-Variable in Zeile 50 wird nur für dialog benötigt. Einer der Parameter gibt die Größe des Menü-Rahmens an, und dort müssen Dinge wie Überschrift, innere Rahmen und OK- sowie Abbruch-Schaltfläche berücksichtigt sein. Der Wert wird einfach durch Ausprobieren ermittelt.

In Zeile 56 wird nun der Parameter-String für den Dialog-Aufruf zusammengebaut und in Zeile 57 mittels eval ausgeführt. Mit read in Zeile 58 wird die Ausgabe des jeweiligen Dialog-Programms in der Variablen REPLY gespeichert.

Die Funktionen TextEingabe, JaNein und Fehler sind unspektakulär, hier wird jeweils nur ein Dialog-Aufruf ausgeführt.

Termine bearbeiten

Auch TerminBearbeiten enthält wenig Neues: In Zeile 84 und 85 werden die Parameter-Listen für AuswahlMenue angelegt, die alle zum Bearbeiten eines Termin-Eintrags notwendigen Möglichkeiten enthalten (Abbildung 2). Ab Zeile 90 erfolgt die Auswertung der Auswahl.

Abbildung 2: Anpassen eines Termins: Mit dialog lassen sich Einstellungen sehr übersichtlich ändern, die Menüführung nimmt vielen unerfahrenen Benutzern die Angst vor der Kommandozeile

Beim Löschen eines Termins wird vom Benutzer noch einmal eine Bestätigung eingeholt, das eigentliche Entfernen des Termineintrags erfolgt in Zeile 100 bis 106 durch Aufrücken der dahinter stehenden Einträge. Diese Lösung ist nicht die eleganteste, da bei langen Listen sehr viele Einträge aufgerückt werde müssen, hier bleibt Raum für Ihre Verbesserungen. Als Tipp: Auch hier lässt sich mit der Variablen IFS und Tabulator-getrennten Listen viel machen.

Eine weitere Besonderheit steckt in der Datumsänderung. Leider erkennt date das Datum in der herkömmlichen Schreibweise als Tag, Monat und Jahr mit Punkten getrennt nicht. Deshalb erfolgt die Eingabe in unserem gepackten Format mit Jahr, Monat und Tag, unterteilt mit Minus. Es wird allerdings nicht geprüft, ob die Eingabe auch tatsächlich ein Datum ist. Die Rückwandlung übernimmt wiederum date für uns, indem wir als Ausgabe-Format das gepackte Datumsformat angeben.

Hauptprogramm

Das Einlesen der Termin-Liste unterscheidet sich kaum vom termin-server. Nur werden in den Zeilen 146 bis 150 die Daten in einzelne Arrays verpackt. Lässt sich die Liste nicht öffnen, wird eine leere angelegt und die lokale Mail-Adresse des Benutzers bestimmt.

In der Endlosschleife ab Zeile 159 wird das Hauptmenü generiert. Wie schon bei TerminBearbeiten werden Tastenkürzel und Beschreibung in separaten Variablen zusammengefasst und an AuswahlMenu übergeben. Beim Speichern wird so notwendig die Termin-Liste neu angelegt; ist sie nicht beschreibbar, wird entsprechend gewarnt. Das Speichern selbst geschieht in Zeile 179 bis 185: Nach der Mail-Adresse werden die einzelnen Terminzeilen geschrieben.

Finale

In den vergangenen sechs Folgen der Programming Corner haben wir Ihnen die Grundlagen der Bash-Programmierung gezeigt. Damit sind Sie gut gerüstet, um vielseitige Skripte für den Alltag zu schreiben. Zusammen mit einem ansprechenden Design, wie es die drei Dialog-Programme bieten, lassen sich auch umfangreiche Eingaben verständlich abfragen. Doch Shell-Programmierung ist mehr als nur Bash-Scripting: Erst mit dem schier unerschöpflichen Werkzeugkasten der GNU-Tools wird sie zu einer leistungsfähigen und vielseitigen Programmiersprache.

Listing 1

termin-server

01  #!/bin/bash
02
03  TerminWarnung ()
04  {
05    if [ "$6" != "" ]; then
06      MailParm="-c $6"
07    fi
08
09    mail $MailParm -s "Termin am `date -d \"$2\" \"+%a, %d.%m.%Y\"`: ${4}" $1 << EOF
10  $5
11  EOF
12  }
13
14  if [ ! -r ~/.termine ]; then
15    echo "termin-server: Kann Termin-Liste nicht öffnen ($HOME/.termine)"
16  fi
17
18  IFS=$'\r'
19
20  read -a TerminListe << EOF
21  `grep -v "^#" ~/.termine | tr "\n" "$IFS"`
22  EOF
23
24  MailAdresse="${TerminListe[0]}"
25
26  IFS=$'\t'
27
28  i=1
29
30  while [ "${TerminListe[$i]}" != "" ]; do
31    set – ${TerminListe[$i]}
32
33    Vorwarnung="`date -d \"$1 $2 days ago\" +%Y%m%d`"
34    if [ "$Vorwarnung" -le "`date +%Y%m%d`" -a "$1" -ge "`date +%Y%m%d`" ]; then
35      TerminWarnung $MailAdresse $@L: *
36    fi
37
38    i=$[$i+1]
39  done

Listing 2

termin-client

001  #!/bin/bash
002  if [ -n "`which gdialog 2>/dev/null`" ]; then
003    Dialog="`which gdialog`"
004    DialogRedirect="2>~/.termine.in"
005  elif [ -n "`which kdialog 2>/dev/null`" ]; then
006    Dialog="`which kdialog`"
007    DialogRedirect="1>~/.termine.in"
008  elif [ -n "`which dialog 2>/dev/null`" ]; then
009    Dialog="`which dialog`"
010    DialogRedirect="2>~/.termine.in"
011  else
012    echo "Sie benötigen dialog, kdialog oder gdialog,"
013    echo "um dieses Programm benutzen zu können."
014    exit 1
015  fi
016
017  COLUMNS=80
018  LINES=24
019
020  AuswahlMenue ()
021  {
022    Titel="\"$1\""
023    Tasten="$2"
024    Namen="$3"
025
026    IFS=$'\t'
027    set – $Tasten
028    i=1
029    while [ "$#" -gt "0" ]; do
030      Taste[$i]="\"$1\""
031      shift
032      i=$[$i+1]
033    done
034    Elemente="$i"
035
036    IFS=$'\t'
037    set – $Namen
038    i=1
039    while [ "$#" -gt "0" ]; do
040      Name[$i]="\"$1\""
041      shift
042      i=$[$i+1]
043    done
044
045    Menu=""
046    for ((i=1; $i<$Elemente; i=$[$i+1])); do
047      Menu="${Menu}${Taste[$i]}${IFS}${Name[$i]}${IFS}"
048    done
049
050    Overhead="7"
051    if [ "$[$Elemente+$Overhead]" -lt "$LINES" ]; then
052      DisplayLines="$[$Elemente+$Overhead]"
053    else
054      DisplayLines="$LINES"
055    fi
056    DialogParms="--menu $Titel $DisplayLines $[$COLUMNS-8] $[$Elemente-1] $Menu"
057    eval $Dialog $DialogParms $DialogRedirect
058    read < ~/.termine.in
059  }
060  TextEingabe ()
061  {
062    DialogParms="--inputbox \"$1\" 8 $[$COLUMNS-8] \"$2\""
063    eval $Dialog $DialogParms $DialogRedirect
064    read < ~/.termine.in
065    if [ "$REPLY" = "" ]; then
066      REPLY="$2"
067    fi
068  }
069  JaNein ()
070  {
071    DialogParms="--yesno \"$1\" 5 $[$COLUMNS-8]"
072    eval $Dialog $DialogParms $DialogRedirect
073  }
074  Fehler ()
075  {
076    DialogParms="--msgbox \"$1\" 5 $[$COLUMNS-8]"
077    eval $Dialog $DialogParms $DialogRedirect
078  }
079  TerminBearbeiten ()
080  {
081    Nr="$1"
082
083    while true; do
084      Keys="d${IFS}v${IFS}b${IFS}t${IFS}c${IFS}l${IFS}z${IFS}"
085      Menu="Datum: `date -d ${Datum[$Nr]} +%d.%m.%Y`${IFS}Vorwarnung: ${Vorwarnzeit[$Nr]} Tage${IFS}Betreff: ${Bezeichnung[$Nr]}${IFS}Text: ${Text[$Nr]}${IFS}CCs: ${CCs[$Nr]}${IFS}Löschen${IFS}Zurück"
086
087      AuswahlMenue "Termin bearbeiten:" "$Keys" "$Menu"
088      Auswahl="$REPLY"
089
090      case $Auswahl in
091        z|"")
092          break
093          ;;
094        l)
095          JaNein "Termin löschen: Sind Sie sicher?"
096
097          if [ "$?" = "0" ]; then
098            Termine=$[$Termine-1]
099
100            for ((i=$Nr; $i < $Termine; i=$[$i+1])); do
101              Datum[$i]=${Datum[$[$i+1]]}
102              Vorwarnzeit[$i]=${Vorwarnzeit[$[$i+1]]}
103              Bezeichnung[$i]=${Bezeichnung[$[$i+1]]}
104              Text[$i]=${Text[$[$i+1]]}
105              CCs[$i]=${CCs[$[$i+1]]}
106            done
107          fi
108          break
109          ;;
110        d)
111          TextEingabe "Datum, in der Form yyyy-mm-dd:" "`date -d ${Datum[$Nr]} +%Y-%m-%d`"
112          Datum[$Nr]="`date -d $REPLY +%Y%m%d`"
113          ;;
114        v)
115          TextEingabe "Vorwarnzeit in Tagen:" "${Vorwarnzeit[$Nr]}"
116          Vorwarnzeit[$Nr]="$REPLY"
117          ;;
118        b)
119          TextEingabe "Kurze Bezeichnung des Termins:" "${Bezeichnung[$Nr]}"
120          Bezeichnung[$Nr]="$REPLY"
121          ;;
122        t)
123          TextEingabe "Ausführliche Beschreibung, Anmerkungen:" "${Text[$Nr]}"
124          Text[$Nr]="$REPLY"
125          ;;
126        c)
127          TextEingabe "Zusätzliche Empfänger der Warnung (Komma-Trennung):" "${CCs[$Nr]}"
128          CCs[$Nr]="$REPLY"
129          ;;
130      esac
131    done
132  }
133
134  ##### Hauptprogramm #####
135
136  if [ -r ~/.termine ]; then
137    IFS=$'\r'
138    read -a TerminListe << EOF
139  `grep -v "^#" ~/.termine | tr "\n" "$IFS"`
140  EOF
141
142    IFS=$'\t'
143    i=1
144    while [ "${TerminListe[$i]}" != "" ]; do
145      set – ${TerminListe[$i]}
146      Datum[$i]=$1
147      Vorwarnzeit[$i]=$2
148      Bezeichnung[$i]=$3
149      Text[$i]=$4
150      CCs[$i]=$5
151      i=$[$i+1]
152    done
153    Termine="$i"
154  else
155    MailAdresse="${LOGNAME}@localhost"
156    Termine=1
157  fi
158
159  while true; do
160    IFS=$'\t'
161    Keys="0${IFS}\$Keys${IFS}n${IFS}q${IFS}x${IFS}"
162    Menu="Adresse:  ${MailAdresse}\$Menu${IFS}Neuer Termin${IFS}Speichern und Ende${IFS}Ende ohne Speichern"
163    for ((i=1; $i < $Termine; i=$[$i+1])); do
164      Keys="${Keys/\$Keys/${IFS}$i\$Keys}"
165      Menu="${Menu/\$Menu/${IFS}`date -d ${Datum[$i]} +%d.%m.%Y`: ${Bezeichnung[$i]}\$Menu}"
166    done
167    Keys="${Keys/\$Keys}"
168    Menu="${Menu/\$Menu}"
169
170    AuswahlMenue "Hauptmenü" "$Keys" "$Menu"
171    Auswahl="$REPLY"
172
173    case $Auswahl in
174      q)
175        if [ ! -e ~/.termine ]; then
176          touch ~/.termine 2>/dev/null
177        fi
178        if [ -w ~/.termine ]; then
179          echo "$MailAdresse" > ~/.termine
180          IFS=$'\t'
181          for ((i=1; $i < $Termine; i=$[$i+1])); do
182            cat >> ~/.termine << EOF
183  ${Datum[$i]}${IFS}${Vorwarnzeit[$i]}${IFS}${Bezeichnung[$i]}${IFS}${Text[$i]}${IFS}${CCs[$i]}
184  EOF
185          done
186          break
187        else
188          Fehler "Die Termine-Datei (~/.termine) lässt sich nicht schreiben."
189        fi
190        ;;
191      x|"")
192        exit 1
193        ;;
194      n)
195        Datum[$Termine]="`date +%Y%m%d`"
196        Vorwarnzeit[$Termine]="2"
197        Bezeichnung[$Termine]=""
198        Text[$Termine]=""
199        CCs[$Termine]=""
200        Termine=$[$Termine+1]
201        TerminBearbeiten $[$Termine-1]
202        ;;
203      0)
204        TextEingabe "Ihre Email-Adresse:" "$MailAdresse"
205        MailAdresse="$REPLY"
206        ;;
207      @L: *)
208        TerminBearbeiten $Auswahl
209        ;;
210    esac
211  done
Einem Freund empfehlen    Druckansicht beenden Bookmark and Share
Kommentare