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




