Original von http://uebb.cs.tu-berlin.de/~krischan/adabasd/TclCrash.html

Ein Crash-Kurs für Tcl, Tk und AdabasTcl

Tcl

Tcl ist eine sogenannte Script-Sprache, entwickelt von John Ousterhout an den Sun Microsystems Labaratory. Es ist eine typischerweise interpretierte Sprache, die überall dort eingesetzt werden kann, wo eine Kommandosprache als Bestandteil einer Applikation gebraucht wird (z.B. ein Debugger, Editor, aber auch ein Internet-Server). Daher stammt auch das Acronym Tool Command Language. Über die Jahre hat sich aber als Hauptanwendung ein ,,Standalone''-Tcl-Interpreter (der tclsh) herauskristallisiert.

Tcl ist von Beginn an so konzipiert worden, daß es einfach erweitert werden kann. Dabei ist eine Erweiterung eine Sammlung von in einer anderen Programmiersprache definierten Kommandos, die als Tcl-Kommandos aufgerufen werden können. Viele solcher Erweiterungen existieren, die bekanntesten sind wohl Tk (ein Toolkit zur Programmierung von GUI-Applikationen) und Expect (zur Ansteuerung von anderen Programmen). Außerdem gibt es kaum eine relationale Datenbank, zu der keine Schnittstelle existiert.

Hier ein Bild, das die Einordung der Erweiterungen deutlich machen soll. Es ist außerdem der jeweilige Tcl-Interpreter angegeben, der die Erweiterungen bereits integriert hat.

Kommandos und Parameter

Die Arbeit eines Tcl-Interpreters ist denkbar einfach: Er nimmt sich jeweils eine Zeile, zerlegt sie in Worte, betrachtet das erste Wort als Kommando sowie alle nachfolgenden Worte als dessen Parameter, und ruft das gegebene Kommando mit den Parametern auf. Worte werden begrenzt durch Leerzeichen, Tabulatoren oder Zeilenwechsel.

Wenn ein Zeichen, das normalerweise ein Wort beenden würde, Teil eines Wortes sein soll, kann das durch Voranstellen eines Backslashes vor das entsprechende Zeichen geschehen, durch Umklammern des gesamten Wortes durch doppelte Hochkommata oder durch geschweifte Klammern. Geschweifte Klammern schachteln, Hochkommata schachteln nicht.

Um die Sprache möglicht einfach zu halten, gibt es in Tcl nur einen Datentyp, die Zeichenfolge (ein String). Eine Folge von Buchstaben muß nicht mit Hochkommata umklammert werden, um eine Zeichenfolge zu bilden, jedenfalls solange sie keine Leerzeichen enthält. Und die umklammernden Kommata (oder auch geschweifte Klammern) sind nicht Teil der Zeichenkette. Was Tcl betrifft, macht es daher keinerlei Unterschied, ob 50 mit 10 addieren werden soll oder ,,50'' mit {10}.

Hallo Welt

Der große Moment naht, wo wir genügend wissen, um das erste Tcl-Programm auszuführen. Mit der Information, daß das Kommando puts einen String auf die Standardausgabe schreibt (ähnlich wie das Unix-Kommando echo), rufen wir also den Interpreter auf und tippen es ein:

# tclsh8.0
% puts Hello World
can not find channel named "Hello"
% puts "Hello World"
Hello World
% puts {Hello World}
Hello World
% puts Hello\ World
Hello World
% #

Der erste Versuch lieferte einen Fehler, da puts den auszugebenden String als einen Parameter erwartet, wir ihn jedoch wegen des Leerzeichens als zwei Parameter übergeben haben. Bei einem Aufruf mit zwei Parametern muß der erste jedoch eine zum Schreiben geöffnete Datei (z.B. stdout) sein.

Die letzte Zeile erfordert auch noch eine Interpretation:
Am Ende der Tcl-Session wurde (hinter dem zweiten Prozentzeichen, leider ohne sichtbares Echo am Bildschirm) Control-d eingegeben als Zeichen, daß keine weitere Eingabe kommt. Der Tcl-Interpreter beendete seine Arbeit und der Unix-Shell antwortete mit seinem nächsten Prompt.

Tcl-Programme

Ein Tcl-Skript wird direkt interpretiert, also nicht etwa in einem extra Lauf in Maschinencode compiliert, um dann als ausführbares Programm gestartet zu werden.
Wenn ein Tcl-Skript in einer Datei vorliegt (hier z.B. hello.tcl), kann der Dateiname dem Interpreter als Argument übergeben werden. Er führt dann alle darin enthaltenen Kommandos aus und beendet sich anschließend. Hier der Inhalt von hello.tcl:

# cat hello.tcl
puts "Hello World"
# tclsh8.0 hello.tcl
Hello World
#

Wenn wir die Ausführungsrechte des Programms setzen und am Anfang der Datei einige magische Zeilen einführen, kann hello.tcl wie jedes andere Programm durch Namensnennung aufgerufen werden.

# cat hello.tcl
#!/bin/sh
# the next line restarts using tclsh \\
exec tclsh8.0 "$0" "$@"

puts "Hello World"
# chmod a+x hello.tcl
# hello.tcl
Hello World
#

Ersetzungen

Bevor das Kommando (also das erste Wort) tatsächlich aufgerufen wird, führt der Tcl-Interpreter noch zwei Arten von Ersetzungen durch. Bei Dollarzeichen gefolgt von einem Namen wird der Inhalt einer Variablen mit diesem Namen ersetzt, bei eckigen Klammern wird alles zwischen den Klammern als eigenes Tcl-Kommando angesehen und das Ergebnis dieses Komamndos wird ersetzt.

Variablennamen können entweder aus einem einfachen Namen bestehen (z.B. x) oder aus zwei Namen, wobei der zweite in runden Klammern steht (z.B. x(y)). Letzteres ist der Zugriff auf ein sogenanntes assoziatives Array; d.h. x ist ein Feld, auf dessen Einträge mit Schlüsseln zugegriffen werden kann, die nicht unbedingt numerisch sein müssen.

Hier unser zweites Tcl-Programm, das beide Arten der Ersetzung vorführt.

# cat subst.tcl

puts "Hello $env(LOGNAME) at [pwd]"

# tclsh8.0 subst.tcl
Hello krischan at /u/krischan
#

Bei Programmstart initialisiert der Interpreter eine globale Variable env mit allen Umgebungsvariablen. Mit $env(LOGNAME) greife ich also auf den Wert der Umgebungsvariable LOGNAME zu. pwd ist ein Tcl-Kommando, das wie das Shell-Kommando gleichen Namens die aktuelle Directory liefert.

Ausdrücke

Da in Tcl selbst nur der Datentyp String bekannt ist, werden vom Tcl-Interpreter selbst auch keine mathematischen Ausdrücke berechnet. Dazu gibt es das Kommando expr, welches alle übergebenen Argumente zu einer Zeichenkette aneinanderhängt und anschließend mit einer zu C identischen Syntax berechnet.

Boolesche Ausdrücke (ebenfalls identisch zu C) müssen einem numerischen Wert ergeben; 0 wird dabei als falsch betrachtet, alles andere als wahr.

Auch Zeichenketten können innerhalb eines Ausdrucks benutzt werden, z.B. in einem Vergleich. Dann müssen sie jedoch in doppelte Hochkommata gesetzt werden.

set x [expr {4*28-int(12.2)}]
puts [expr {$x % 2 ? "Odd" : "Even"}]

Variablen

Durch die Dollarsubstitution kann auf den Inhalt einer Variablen bequem zugegriffen werden. Mit dem Kommando set kann einer Variablen ein Wert zugewiesen werden. Hierzu wird set mit 2 Parametern aufgerufen: dem Variablennamen und dem Wert, den die Variable annehmen soll. Bei Aufruf von set mit nur einem Parameter wird der derzeitige Wert der Variablen zurückgeliefert. Es gibt also zwei Wege, den Wert einer Variablen zu erfragen: Durch die Dollarsubstitution und durch Aufruf von set.

# cat var.tcl

set user $env(LOGNAME)
set pwd  [pwd]
puts "Hello [set user] at $pwd"

# tclsh8.0 var.tcl
Hello krischan at /u/krischan
#

Kontrollstrukturen

Tcl besitzt ähnliche Kontrollkonstrukte wie C oder die Bourne-Shell. Ein (allerdings eher philosophischer) Unterschied ist, daß diese Konstrukte in Tcl auch nur normale Kommandos sind. Es gilt also z.B. ein if Kommando, daß mindestens 2 Parameter übergeben bekommt. Der 1 Parameter wird dann als ein boolescher Ausdruck berechnet und falls er wahr ergibt, wird der zweite Parameter als Tcl-Skript betrachtet und ausgeführt.

Hier ein Beispiel einer for Schleife, die (genau wie in C) aus Initialisierung, Schleifenbedingung, Inkrementanweisung und dem zu wiederholenden Skript besteht. Der zweite Parameter (Schleifenbedingung) muß ein gültiger Ausdruck sein, da er dem expr Kommando zur Abarbeitung übergeben wird. Alle anderen Parameter werden als Folgen von Tcl-Anweisungen interpretiert.

# cat for.tcl

for {set i 0} {$i < 5} {incr i} {
  puts "Hello $env(LOGNAME) at [pwd]"
}

# tclsh8.0 for.tcl
Hello krischan at /u/krischan
Hello krischan at /u/krischan
Hello krischan at /u/krischan
Hello krischan at /u/krischan
Hello krischan at /u/krischan
#

Prozeduren

Prozeduren werden mit dem proc Kommando erzeugt, welches drei Argumente erwartet, den Namen der Prozedur sowie die Parameterliste und die Anweisungssequenz. Anschließend kann die neue Prozedur genauso wie jedes vordefinierte Tcl-Kommando benutzt werden.

Fast immer wird eine Prozedur im folgenden Format definiert:

proc procName {param1 param2} {
  statement1
  statement2
}

Wichtig ist insbesondere die öffnende Klammer am Ende der ersten Zeile. Würde sie erst auf der zweiten Zeile stehen, würde der Tcl-Interpreter das proc Kommando mit nur zwei Parametern aufrufen (Kommandoname und Parameterleite), was jedoch von diesem mit einer Fehlermeldung quittiert wird.

# cat proc.tcl

proc hello {count channel} {
  global env
  for {set i 0} {$i < $count} {incr i} {
    puts $channel "Hello $env(LOGNAME) at [pwd]"
  }
}
hello 3 stdout

# tclsh8.0 proc.tcl
Hello krischan at /u/krischan
Hello krischan at /u/krischan
Hello krischan at /u/krischan

Innerhalb einer Prozedur interpretiert Tcl Variablennamen prinzipiell als prozedurlokal. Um innerhalb einer Prozedur (sowohl lesend als auch schreibend) auf eine globale Variable zugreifen zu können, werden durch das global Kommando die angegebenen Variablen auch innerhalb der Prozedur bekannt gemacht.

Immer mal wieder vergesse ich eine global Anweisung. Beim Zugriff auf eine globale Variable wird das ja sofort durch eine Fehlermeldung (z.B. can't read ,,env(LOGNAME)'': no such variable) quittiert. Schlimmer ist es bei einer Zuweisung mittels set. Hier wird dann einer prozedurlokalen Variable der Wert zugewiesen, der Wert der globalen Variablen ändert sich jedoch nicht. Und später an einer anderen Stelle bleibt dann das Rätsel zu lösen, warum die Zuweisung anscheinend nicht ausgeführt wurde.

Listen

Wie bereits mehrfach erwähnt, besitzt Tcl nur einen Datentyp, die Zeichenfolge. Es stehen jedoch noch die Möglichkeiten des Arrays und der Liste zur Verfügung, um Daten zu strukturieren. Während Arrays bereits im Abschnitt Variablen kurz beschrieben wurden, folgt hier eine Übersicht über Listen.

Vom Aufbau her sind Tcl-Kommandos und Listen relativ ähnlich. Es sind beides Zeichenfolgen, die durch Leerzeichen getrennte Elemente enthalten. Auch hier kann das Problem des Leerzeichens in einem Element wieder durch entsprechende Escapezeichen oder Klammerungen gelöst werden. Es gibt aber auch Kommandos, die Listen zusammenbauen oder zerlegen, ohne daß man sich einen Kopf darum machen muß, ob sich eventuell ein Leerzeichen in einem Listenelement befindet.

Als Beispiel wird im folgenden eine zweidimensionale Liste (also eine Tabelle) erzeugt und anschließend ausgegeben. Um möglichst viele verschiedene Listenoperationen vorzuführen, wird (fast) jede Zeile der Tabelle mit einem anderen Befehl erzeugt. Zunächst wird die Tabelle durch set initialisiert als einelementige Liste, deren einziges Element eine zweielementige Liste mit den beiden ,,Überschriften'' unserer Tabelle ist. Dann werden zwei Zeilen mittels lappend an die Tabelle hinten angehängt. Die nächste Zeile wird durch linsert explizit an die dritte Position eingeschoben, um anschließend durch lreplace ersetzt zu werden. Da diese beiden Operationen nicht wie lappend auf der Liste mit dem gebenen Namen arbeiten, sondern eine modifizierte Liste zurückliefern, wird das Ergebnis wieder der Listenvariablen zugewiesen.

Die Ausgabe der Tabelle erfolgt mit Hilfe des foreach Kommandos, das nach und nach jedes Listenelement seines zweiten Parameters der Variable mit dem Namen des ersten Parameters zuweist und damit dann die Anweisungsfolge des dritten Parameters aufruft.

format betrachtet den ersten Parameter als C-ähnlichen Formatstring und formatiert die folgenden Parameter entsprechend. Die Kombinations aus puts und format entspricht damit der C-Funktion printf().

# cat list.tcl

set langs [list [list LANGUAGE CREATOR]]
lappend langs [list Tcl  "John Ousterhout"]
lappend langs      {Perl "Larry Wall"}
set langs [linsert  $langs 2   [list C      "Kernighan/Ritchie"]]
set langs [lreplace $langs 2 2 [list Python "Guido van Rossum"]]

foreach l [lrange $langs 0 2] {
  puts [format "%-8s %s" [lindex $l 0] [lindex $l 1]]
}

# tclsh8.0 list.tcl
LANGUAGE CREATOR
Tcl      John Ousterhout
Python   Guido van Rossum
#

Kommunikation mit der Umwelt

Um eine Datei zu öffnen, gibt es das open Kommando, welches einen Dateidesriptor zurückliefert, der in nachfolgenden Aufrufen von puts, read und close benutzt werden kann.

Mit cd, pwd und glob und file gibt es die Möglichkeit, auf das Dateisystem und einzelne Dateien zuzugreifen.

Um Kommandos auf Betriebssystemebene aufzurufen, gibt es das Kommando exec. Das Kommando wird ausgeführt und der Returnwert wird zurückgeliefert. Wenn man mit dem gestarteten Programm über seine Ein-/Ausgabe kommunizieren will, wird es (wie im folgenden Beispiel) als Kommando-Pipeline geöffnet, indem beim open statt des Dateinamens das Pipe-Symbol (|) gefolgt von dem Kommando angegeben wird.

# cat exec.tcl

set f [open "|ls -l /tmp"]
set output [read $f]
close $f
set lines [split $output "\n"]
puts [lindex $lines 0]

# tclsh8.0 exec.tcl
total 60
#

Eine Anmerkung noch zum im Beispiel verwendeten split Kommando. Es erzeugt eine Liste, indem es seinen ersten Parameter an dem als zweiten Parameter gegebenen gegebenen Zeichen auftrennt. Das dazu inverse Kommando join macht aus einer Liste wieder eine Zeichenfolge.

Tk

Tk ist wohl die bekannteste Erweiterung von Tcl. Sie stammt ebenfalls von John Ousterhout und wird auch vom Tcl-Team bei Sun weiterentwickelt. Es ist ein platformübergreifendes Toolkit (deshalb Tk) zum Entwickeln von Applikationen mit graphischer Benutzeroberfläche.

Wer jemals ein Programm für eine graphische Benutzeroberfläche in C mit z.B. dem Xt-Toolkit programmiert hat, weiß, daß das ein recht komplexes Unterfangen ist. Es müssen die entsprechenden Objekte erzeugt und mit Hilfe eines Geometrie-Managers angeordet werden; außerdem sind entsprechenden Events (z.B. Mausklicks oder Tastendrücke) Prozeduren zuzuordnen, die jeweils aufgerufen werden sollen. Am Ende des Programms wird dann in eine Endlosschleife eingetreten, in deren Rumpf jeweils ein Event abgearbeitet wird.

In Tcl/Tk muß das auch alles geleistet werden, um eine Applikation zu erstellen. Nur kann es durch eine Vielzahl von mächtigen Widgets mit einer Unzahl von Konfigurationsoptionen leichter und kompakter formuliert werden. Auch zum Geometrie-Managment stehen drei verschiedenen Manager zur Auswahl, jeder spezialisiert auf sein Einsatzgebiet. Und die genannte Endlosschleife ist implizit, d.h., der Tk-Interpreter (wish genannt für WIndowing SHell) arbeitet permanent eintreffende Events ab, wenn keine Befehle von seiner Standardeingabe kommen.

Widgets

Durch Tk werden eine Menge von Tcl-Kommandos definiert, die jeweils ein Element der graphischen Oberfläche kreieren. Diese Elemente werden im allgemeinen Widgets genannt; dieser Ausdruck stammt aus der Unix-Welt und ist eine Verkürzung von Window Gadget, was soviel wie Fenster-Dingsbums heißt. Ein Widget entspricht ungefähr einem Control aus der Windows-Welt.

Es gibt in Tk 15 verschiedene Widgets, aus denen die Oberfläche zusammengesetzt werden kann. Dabei hat selbst das wohl simpelste Widget, das Label zum Darstellen eines Textes, bereits 22 Konfigurationsparameter. Da Tk für die meisten Parameter bereits vernünftige Defaultwerte vorgibt, werden jedoch normalerweise nur ein paar Parameter tatsächlich angegeben: beim Label wird wohl immer der Text spezifiziert, der angezeigt werden soll.

Der erste Parameter eines Widget-Kommandos ist der Name des zu erzeugenden Widgets. Aus dem Namen kann die Position innerhalb der Widget-Hierarchie ersehen werden ähnlich einem Dateisystem. Als Trenner der einzelnen Elemente dient hier allerding der Punkt und nicht ein Schrägstrich. Das oberste Fenster einer Applikation heißt also ., ein darin befindliches Buttonleiste könnte .buts heißen, und der Exit-Button darin würde dann .buts.exit genannt.

Konfigurationsparameter von Widget-Kommandos werden grundsätzlich in zwei Worten hingeschrieben: der Name des Parameters mit einem führenden Strich und anschließend der Wert des Parameters. Hier als Beispiel ein Label, welches ob seiner wichtigen Botschaft eine rote Hintergrundfarbe erhält:

label .lab -text "Press F3 to exit" -background red

Zusätzlich zu dem Objekt auf dem Bildschirm wird ein neues Tcl-Kommando erzeugt mit dem Namen des Widgets. Diese Widget-Kommandos haben prinzipiell als ersten Parameter ein Unterkommando, das auf dem Widget ausgeführt werden soll. Das Unterkommando entspricht ungefähr dem Aufruf einer Methode in C++.

Mit Hilfe des Widget-Kommandos kann das erzeugte Widget z.B. noch rekonfiguriert werden kann. Um obiges Label noch auffälliger zu gestalten, bekommt es nun noch einen Rand.

.lab configure -relief groove -borderwidth 2

Hallo nochmals

Auch bei graphischen Applikationen wird gern das ,,Hallo Welt'' Programm als erstes Beispiel genommen. Da in Tcl/Tk solche Sachen wirklich sehr einfach und kompakt formuliert werden können, schrauben wir die Anforderungen hoch: Es soll nicht nur ein Fenster mit der freundlichen Begrüßung erscheinen, sondern bei einem Mausklick soll die Begrüßung auch noch auf die Standardausgabe geschrieben werden. Außerdem soll ein Exit-Knopf zum Verlassen der Applikation vorhanden sein.

Das Tcl-Programm ist jedenfalls kürzer als die obige Anforderungsdefinition.

# cat button.tcl

button .b -text "Hello world" -command {puts "Hello $env(LOGNAME)"}
button .q -text "Exit"        -command exit
pack .b .q -side top -fill x

# wish8.0 button.tcl
Hello krischan
#

Durch Verwendung des -command Parameters wird ein Tcl-Skript definiert, welches ausgeführt werden soll, falls der Benutzer den Button mit der Maus anklickt. Diese Skripte werden dann später auf globalem Level ausgeführt; daher ist kein global Kommando nötig.

Das pack Kommando ruft einen der drei Geometrie-Manager auf. Der Packer positioniert die übergebenen Widgets immer an eine der vier Seiten des übergeordneten Fensters (in unserem Beispiel an die obere Seite des Toplevels mit dem Namen ., welches vom wish automatisch angelegt wird).

Events

Wie im letzten Beispiel ersichtlich, besitzt das Label einen -command Parameter, mit dessen Hilfe ein Tcl-Kommando als Antwort auf ein bestimmtes Ereignis festgelegt werden kann.

Normalerweise geschieht das mit dem bind Kommando, welches drei Parameter bekommt: ein Widget, ein Event und ein Tcl-Skript. Nach Aufruf des Kommandos wird immer, wenn das genannte Event im gegebenen Widget eintritt, das Tcl-Skript abgearbeitet. Hier ein Beispiel.

bind . <F3> {puts "Bye bye"; exit}

Ein Event kann u.a. ein Tastendruck (<a>, <space> oder <F1>) sein, eine Bewegung der Maus (<Motion>) oder ein Mausklick (<1> oder <3>). Es gibt aber auch Events, die nicht so direkt mit Aktionen des Benutzers verknüpft sind wie z.B. <Destroy>, wenn ein Fenster geschlossen wird.

Scrollbars

Scrollbars sind Widgets, die dem Benutzer die Möglichkeit geben, den Inhalt eines anderen Fensters so zu verschieben, daß der von ihm gewünschte Ausschnitt sichtbar ist. Scrollbars sind also horizontal oder vertikal einem Fenster zugeordnet; dieses Fenster muß Scrollen unterstützen (das tun die entry, listbox, text und canvas Widgets).

Ein Scrollbar und das anzusteuernde Widget werden über Konfigurationsparameter verknüpft (wie sonst?). Das dabei zugrundeliegende Protokoll wird am einfachsten durch ein Beispiel deutlich.

entry .e -xscrollcommand ".s set"
scrollbar .s -orient horizontal -command ".e xview"

Geometrie-Manager

Um die ganzen erzeugten Widgets auch in einer sinnvollen Anordnung auf dem Bildschirm präsentieren zu können, werden sie von einem Geometrie-Manager verwaltet. Es gibt in Tk drei verschiedene Manager: place, pack und grid.

Mittels place kann ein Widget an bestimmte Stellen des übergeordneten Fensters positioniert werden. Dafür stehen die -x und -y Parameter zum absoluten und die -relx und -rely Parameter zum relativen Positionieren zur Verfügung. Die folgenden Kommandos zeigen jeweil ein Label an der Stelle 1 cm unterhalb und 1 cm rechts der linken oberen Ecke sowie in der Mitte an.

place .hallo -x 1c -y 1c
place .welt  -relx 0.5 -rely 0.5

Mit dem pack Kommando kann ein Widget an eine der vier Seiten des übergeordneten Fensters positioniert werden. Ein Beispiel hierfür wurde ja bereits im Hallo-Welt-Beispiel gezeigt.

Das grid Kommando verwaltet die Widgets in einer zweidimensionalen Tabelle (dem Gitter), in dem mit Hilfe der -row und -column Parameter eine Position innerhalb der Tabelle spezifiziert wird. Durch den -sticky Parameter kann eine Reihe von Himmelsrichtungen angegeben werden, zu denen das Widget seinen in der Tabelle zur Verfügung stehenden Platz ausdehnt. Durch grid rowconfigure bzw. grid columnconfigure kann bestimmt werden, welche Zeilen/Spalten zusätzlichen Platz zugesprochen bekommen, falls das Fenster durch den Benutzer vergrößert wird.

Ein Beispiel für den grid Geometrie-Manager findet sich im nächsten Abschnitt.

Das Text-Widget zum Beispiel

Die Stärke von Tk ist wohl zum Großteil darin begründet, daß es (neben den simplen Widgets wie button oder label) mit text und canvas zwei sehr mächtige Widgets besitzt, mit deren Hilfe auch komplexe Aufgaben leicht gelöst werden können. Mit dem text Widget kann z.B. mit wenig Aufwand ein HTML-Browser implementiert werden.

Das durch den Aufruf von text erzeugte Widget-Kommando (in unserem Beispiel mit dem Namen .t) besitzt eine Vielzahl von Unterkommandos, mit deren Hilfe der Inhalt des Text-Widgets erfragt oder modifiziert werden kann.

Im Beispiel wird als Reaktion auf den Druck der F5-Taste das Resultat des Aufrufs von .t get 1.0 end auf die Standardausgabe gegeben. Das get Unterkommando liefert den Inhalt des text Widgets in den genannten Grenzen zurück.

# cat text.tcl

proc CommandText {w} {
  text $w -width 40 -height 5 -wrap word \\
    -xscrollcommand ".h set" -yscrollcommand ".v set"
  scrollbar .h -orient horizontal -command "$w xview"
  scrollbar .v -orient vertical   -command ".v yview"

  grid $w -row 0 -column 0 -sticky news
  grid .v -row 0 -column 1 -sticky ns
  grid .h -row 1 -column 0 -sticky ew
  grid rowconfigure    . 0 -weight 1
  grid columnconfigure . 0 -weight 1
}

CommandText .t
bind .t <F3> exit
bind .t <F5> {puts -nonewline [.t get 1.0 end]}

# wish8.0 text.tcl
SELECT nummer, name, wert
  FROM artikel
  WHERE wert = (SELECT MIN (wert)
                  FROM artikel)
#

Es ist übrigens kein Zufall, daß der vom Benutzer eingegebene Text ein SQL-Kommando ist. Im folgenden Abschnitt wird die AdabasTcl-Erweiterung vorgestellt, und an dessen Ende werden wir dasselbe text Widget wiederfinden, nur daß bei Druck der F5-Taste der Inhalt nicht auf die Standardausgabe geschrieben, sondern an die Datenbank geschickt wird.

AdabasTcl

AdabasTcl ist eine Erweiterung von Tcl, die ein Satz von Kommandos zur Verfügung stellt, mit deren Hilfe mit einer Adabas D Datenbank kommuniziert werden kann. Neben der Möglichkeit, einer warmen Datenbank SQL-Kommandos zu schicken, kann insbesondere auch eine Verbindung im sogenannten Utility-Modus aufgebaut werden, mit deren Hilfe die Administration der Datenbank möglich ist.

Eine Erweiterung

Eine Tcl-Erweiterung ist eine dynamisch zum Tcl-Interpreter zuladbare Library (auf Unix als sogenannte shared library mit der Endung .so, unter Windows als dynamic link library mit der Endung .dll).

Das Laden der Library wird durch das Kommando package require initiiert. Da sich die Library unterhalb der DBROOT Hierarchie befindet, muß dem Tcl-Interpreter vorher noch mitgeteilt werden, wo er zu suchen hat.

proc loadAdabasTcl {} {
  global auto_path env
  lappend auto_path [file join $env(DBROOT) lib]
  package require Adabastcl
}

Auf Unix-Systemen existiert ein Tcl-Interpreter, der die Erweiterung bereits statisch dazugeladen enthält, der adabastclsh. In diesem Interpreter stehen die Kommandos der Erweiterung also sofort zur Verfügung. Es schadet aber auch hier keinesfalls, loadAdabasTcl aufzurufen. Ebenso existiert ein Interpreter adabaswish, der sowohl AdabasTcl als auch Tk bereits enthält.

Die Verbindung zur Datenbank

Um mit der Datenbank kommunizieren zu können, muß zunächst eine Verbindung hergestellt werden. Sie wird mit dem adalogon-Kommando hergestellt. Als notwendiger Parameter wird ein sogenannter Connection-String angegeben, normalerweise in der Form user,password. Der Name der Datenbank, zu der die Verbindung aufgebaut werden soll, wird der Umgebung entnommen (also der Umgebungsvariablen SERVERDB oder der xuser-Datei); sie kann aber auch explizit als -serverdb Parameter angegeben werden.

Das adalogon Kommando liefert ein Handle für die Verbindung zurück, das bei anderen Kommandos wie z.B. adacommit als erster Parameter übergeben wird. Eine Verbindung wird geschlossen durch das Kommando adalogoff.

# adabastclsh
% set logon [adalogon demo,demo -serverdb MYDB]
MYDB
% adalogoff $logon

Um eine Verbindung zur Datenbank im Utility-Modus herzustellen, wird als zusätzlicher Parameter -service utility spezifiziert. Dann muß übrigens der Connection-String entweder den Superdba oder den Control-User beschreiben.

Ein Cursor

Um SQL-Statements abarbeiten zu können, muß noch ein Cursor geöffnet werden. Zu einer Datenbankverbindung können mehrere Cursor geöffnet werden durch adaopen. Das Kommando erwartet als einzigen Parameter den von einem vorherigen Aufruf von adalogon erhaltenen Verbindungs-Handle. Es liefert einen Handle für den Cursor zurück, der wiederum bei Kommandos wie adasql oder adafetch als erster Parameter übergeben werden muß.

Es existiert ein globales Array adamsg, in dem der Datenbankkern einige zusätzliche Information wie etwa im Fehlerfall die vollständige Fehlermeldung schreibt. Im Eintrag rows befindet sich die Anzahl der betroffenen Zeilen nach einem Insert, Update oder Delete.

# adabastclsh
% set logon [adalogon demo,demo -serverdb MYDB]
MYDB
% set cursor [adaopen $logon]
cursor1
% adasql $cursor "SELECT * FROM gibtsNicht"
-4004
% set adamsg(errortxt)
UNKNOWN TABLE NAME:GIBTSNICHT
% adaclose $cursor
% adalogoff $cursor
Invalid logonHandle "cursor1"
% adalogoff $logon

Selektieren und Ergebnisse holen

Das adasql Kommando bekommt (in seiner einfachsten Form) zwei Parameter: den Cursor-Handle sowie einen String, der das SQL Kommando enthält. Falls der Datenbankkern eine Fehlersituation erkennt, liefert adasql die Fehlernummer zurück.

Im Erfolgsfall wird der leere String zurückgeliefert. Falls es sich nicht um ein Select-Kommando handelte, ist die Arbeit bereits erledigt. Im der globalen Variable adamsg(rows) kann abgelesen werden, wie viele Zeilen betroffen waren.

Im Falle eines Select-Kommados reicht natürlich die Information, daß das Kommando korrekt war, nicht aus. Schließlich will man die selektierten Ergebnisse auch zu sehen bekommen. Dazu existiert das adafetch Kommando, welches nur mit dem Cursor-Handle aufgerufen jeweils eine Liste mit den Spalten der nächsten Zeile aus der Ergebnismenge liefert. Am Ende der Ergebnismenge angekommen liefert adafetch die leere Liste.

Eine Liste mit den Spaltennamen kann durch Aufruf des Kommandos adacols abgefragt werden.

Ein Ausgabe aller selektierten Werte kann also durch die folgende geschachtelte Schleife erreicht werden.

adasql $cursor "SELECT * FROM artikel"
set header [adacols  $cursor]
set colCnt [llength  $header]
set row    [adafetch $cursor]
while {[llength $row]} {
  for {set ix 0} {$ix < $colCnt} {incr ix} {
    puts [format "%-18s : %s" [lindex $header $ix] [lindex $row $ix]]
  }
  set row [adafetch $cursor]
}

Das adafetch Kommando kann noch mit einem Parameter -command aufgerufen werden. Dann wird automatisch bis zum Ende der Ergebnismenge durchgefetcht und für jede Zeile das gegebene Tcl-Script abgearbeitet. Vor Abarbeitung wird in dem Script jedoch ein @ gefolgt von einer Nummer größer als 0 durch den Wert der entsprechenden Spalte ersetzt; @0 wird durch die Liste der Werte aller Spalten ersetzt. Die äußere der beiden Schleifen im obigen Beispiel kann dadurch eliminiert werden, wie folgendes Beispiel zeigt.

adasql $cursor "SELECT * FROM artikel"
set header [adacols  $cursor]
set colCnt [llength  $header]
adafetch $cursor -command {
  for {set ix 0} {$ix < $colCnt} {incr ix} {
    puts [format "%-18s : %s" [lindex $header $ix] [lindex @0 $ix]]
  }
}

MiniTkQuery

Das folgende Beispiel, ist trotz seiner nur knapp 45 Zeilen eine voll funktionsfähige Miniaturausgabe von TkQuery. Es wird hier zunächst unkommentiert wiedergegeben, da fast alles aus den vorhergehenden Abschnitten bekannt ist. Anschließend soll nur noch die Zeile mit dem regexp Kommando einer genaueren Analyse unterworfen werden.

source text.tcl

proc initAdabas {} {
  global logonHandle cursorHandle argv
  set logonHandle  [adalogon [lindex $argv 1] -serverdb [lindex $argv 0]]
  set cursorHandle [adaopen  $logonHandle]
}

proc quitAdabas {} {
  global logonHandle cursorHandle
  adaclose  $cursorHandle
  adalogoff $logonHandle
  exit
}

proc sendToDB {command} {
  global logonHandle cursorHandle adamsg

  if [catch {adasql $cursorHandle $command} msg] {
    puts "Error $msg: $adamsg(errortxt)"
  } elseif [regexp -nocase "^\[ \t\n]*SELECT" $command] {
    adafetch $cursorHandle -command {puts [join @0 "\t"]}
  } else {
    puts "$adamsg(rows) affected."
  }
}

initAdabas
CommandText .t
bind .t <F3> quitAdabas
bind .t <F5> {sendToDB [.t get 1.0 end]}

Das Problem an dem adasql Kommando ist, daß seinen Rückgabewerten nicht entnommen werden kann, ob es sich um ein Select Kommando oder um ein anderes Kommando, das keine Ergebnismenge erzeugt, handelt. Da der Benutzer in dem Textfenster aber ein beliebiges SQL-Kommando eingeben kann, muß die Art des Kommandos anders herausgefunden werden.

Das Tcl-Kommando regexp liefert wahr zurück, falls der reguläre Ausdruck (gegeben als vorletzter Parameter) im letzten Parameter trifft. Und der obige Ausdruck trifft dann, wenn das Kommando mit einem SELECT beginnt, wobei Leerzeichen davor sowie Groß-/Kleinschreibung ignoriert werden.

Utility-Connection

Das adalogon Kommando besitzt noch einen Parameter, der noch nicht erwähnt wurde, den -service Parameter. Gültige Werte sind user, control oder utility.

Beim Aufbau einer Control-Verbindung muß der Connection-String den Control-User beschreiben, da die aufgebaute Verbindung erweiterte Rechte besitzt (sie kann z.B. auf Tabellen zugreifen, die ein normaler Benutzer nicht sieht).

Der Aufbau einer Utility-Verbindung unterscheidet sich grundsätzlich von den beiden anderen Arten. Das zurückgelieferte Logon-Handle kann nicht für adaopen zum Öffnen eines Cursors genutzt werden, da eine Utility-Verbindung ja auch zu einer Datenbank im kalten Zustand hergestellt werden kann.

Statt dessen existiert ein adautil Kommando, welches genutzt wird, um Kommandos zur Verwaltung der Datenbank an den Datenbankserver zu schicken. Welche Kommandos hier erlaubt sind, ist leider in keinem Manual beschrieben; auf jeden Fall funktionieren die Kommandos STATE, RESTART und SHUTDOWN.

Als Beispiel folgt hier ein Skript für den adabastclsh, das den Datenbankkern vom Zustand Offline in einen warmen Zustand überführt.

if [catch {exec $env(DBROOT)/bin/x_start $env(DBNAME)} msg] {
  puts $msg
  exit
}
if [catch {adalogon $env(DBCONTROL)  -serverdb $env(DBNAME) -service utility} l] {
  puts $l
  exit
}
if [catch {adautil $l restart} msg] {
  puts $msg
  exit
}
adalogoff $l