Version 53 of Einfach Tcl

Updated 2001-12-13 15:07:41

Richard Suchenwirth - This page ("simply Tcl") is in German for even more i18n;-) It is the draft of a planned presentation. First thought of ppt or Word, but somehow I preferred the Wiki - plus allowing at least German speakers from Tclworld to discuss this while I'm writing it - best place your comments etc. at bottom of page! For a French course, see [L1 ]


Tcl (Tool command Language, ausgesprochen "tickel") ist eine einfache, aber m�chtige Programmiersprache. Da Turing-vollst�ndig, kann im Prinzip jede Aufgabe in Tcl gel�st werden, jedoch nicht immer optimal.

Tcl wird (zusammen mit Perl, Python, VB) auch als Scriptingsprache bezeichnet. Zu "System-Programmiersprachen" wie C, Java, Assembler bestehen folgende Unterschiede:

  • nahezu typfrei ("Everything is a string" / intern schon getypt)
  • Variablen m�ssen nicht deklariert werden - automatische Speicherverwaltung
  • Ausf�hrung durch Interpreter (byte-kompilierte Prozeduren)
  • Interaktive Nutzung m�glich, reichhaltige Introspektionsm�glichkeiten
  • Aussagekr�ftige Fehlermeldungen (fast "Online-Hilfe"); Weiterarbeit m�glich
  • Compile-Link-Zyklus entf�llt, daher schnelle Entwicklung
  • Wartbarkeit durch Script-Editieren bis zur Zielmaschine
  • Laufzeitverhalten teilweise schlechter als vollkompilierter Code

Tcl kann mit C/C++/Java-Code f�r laufzeitkritische Teile auf verschiedene Weisen verbunden werden:

  • Einbettung (Applikation enth�lt Tcl-Interpreter)
  • Erweiterung (statischer oder dynamisch gelinkter Code wird f�r Tcl zug�nglich gemacht)
  • Tcl_LinkVar: Bindung von Tcl-Variablen an C-Variablen (int, double, boolean, string)
  • Tcl_CreateObjCommand: Bindung eines Tcl-Kommandos an eine C-Implementation
  • externe Server (exec, open: namenlose bidirektionale Pipes)

Tk (ToolKit) ist die wichtigste Tcl-Erweiterung, ein plattformunabh�ngiger GUI-Toolkit (X11, Windows, Macintosh):

  • extrem einfache GUI-Spezifikation
  • gleiche Quelldatei l�uft auf mehreren Plattformen mit "native look & feel"
  • Leistungsf�hige "Widgets": canvas, text, menu, scrollbar, Buttons...
  • Einfache aber m�chtige Geometrie-Manager: pack, place, grid
  • Tk wird auch mit Perl und Python als GUI-Toolkit verwendet

Weitere bekannte Erweiterungen:

  • [incr Tcl] - OO-Unterst�tzung �hnlich C++
  • Expect - Unterst�tzung von Kommunikation zwischen verschiedenen Rechnern, "Fernsteuerung" von Applikationen
  • BWidget - Erweiterung von Tk um komplexere "Megawidgets" in purem Tcl
  • TclX - Erweiterung um Posix-Systemfunktionalit�ten

Tcl bietet starke Unterst�tzung von Internationalisierung mit Unicode:

  • alle Strings intern in Unicode/UTF-8
  • Stringkonstanten mit \uxxxx k�nnen jeden Unicode darstellen
  • Hin- und Herkonvertierung in viele andere internationale Encodierungen
  • Tk: automatische Zeichensuche, falls der aktuelle Font ein gew�nschtes Zeichen nicht enth�lt
  • Einfache Lokalisierung mit msgcat

Tcl vereint Konzepte aus verschiedenen Software-Welten:

  • LISP/Scheme: Liste als zentrale Datenstruktur, polnische Notation, Code = Daten
  • C: Kontrollstrukturen (for, while, if); Arithmetik
  • Shells: Trennung von Parsing und Ausf�hrung; cd, pwd, glob...
  • SNOBOL/awk: assoziative Arrays (Hashtables)
  • X/...: Eventmodell, ereignisgesteuerte Verarbeitung
  • ?: Traces - Notifikation bei Lesen/Setzen/L�schen einer Variablen

Tcl hat die einfachste Syntax aller Scriptingsprachen:

  • keine Schl�sselw�rter, alle Kommandos werden gleich behandelt
  • Script: Folge von Kommandos, durch \n oder ";" getrennt
  • Kommando: Folge von W�rtern, durch Whitespace getrennt; erstes Wort ist Kommandoname
  • Wort: whitespacelose Zeichenkette, oder gruppiert mit ".." {..}
  • "..": "substituiere und gruppiere mich zu einem Wort"
  • {..}: "gruppiere mich, aber r�hr meinen Inhalt nicht an"
  • Rekursiver Interpreteraufruf f�r [..]-eingebettete Scripts
  • [..]: "evaluiere mich zuerst zu einem String"
  • Variablensubstitution durch $name , Array-Elemente: $name($key)
  • Namensraum-Syntax: ::foo::bar; :: ist der globale Namensraum
  • Alle weitere Syntax liegt in Verantwortung der Kommandos

Kommandos haben folgende Eigenschaften:

  • Implementation in C oder als Tcl-proc
  • Argumente mit Defaultwert oder variabler Anzahl m�glich
  • jedes Kommando parst seinen Input nach eigenem Bedarf
  • Kommandos k�nnen umbenannt und �berladen werden
  • Bytecompilation beim ersten Aufruf, danach Wiederverwendung des Bytecodes
  • Neudefinition zur Laufzeit m�glich (proc ist auch nur ein Kommando)
  • auch Kontrollstrukturen sind "nur" Kommandos, k�nnen �berladen oder erweitert werden
  • alle Variablen von Procs sind lokal, k�nnen aber mit globalen Variablen oder Variablen h�her im Callstack verbunden werden

Strukturierung von Tcl-Applikationen:

  • Auch in Tcl kann man unwartbaren Code schreiben, mu� es aber nicht
  • Explizites Laden anderer Scripte mit source (#include zur Laufzeit)
  • Automatisches Laden anderer Scripte bei Bedarf (auto_index)
  • Modularisierung durch ladbare packages (Script und/oder kompiliert)
  • Daten- und Prozedurkapselung durch Namensr�ume

Datenhaltung in Tcl:

  • Listen mit automatischer Speicherverwaltung
  • Assoziative Arrays (Hashtables): Schl�ssel -> Wert
  • �ber geeignete Schl�ssel ($i,$j) mehrdimensionale Arrays simulierbar
  • Formatierung von Daten aller Art (auch bin�r, mit Nullbytes) in Strings
  • Implizite Datenwandlung Liste <-> String <-> Zahl bei Bedarf

Ablaufsteuerung in Tcl:

  • z.B. konventionell sequentiell bzw. if, for, while, foreach
  • Bindung von Kommandos an GUI-Ereignisse (Maus, Tastatur)
  • Bindung von Kommandos an Datei-Ereignisse (fileevent)
  • Bindung von Kommandos an Timer-Ereignisse (after)
  • Bindung von Kommandos an Daten-Ereignisse (Variablen-Trace)

Die wichtigsten Kommandos:

  • set varname value ;# Wertzuweisung
  • expr mathexpression ;# Arithmetik und Vergleiche
  • proc name arglist {body} ;# Definition einer Prozedur
  • if {condition} {body}
  • while {condition} {body}
  • foreach iterator liste {body} ;# Iteration �ber Liste
  • for {set i 0} {$i<$max} {incr i} {...} ;# analog zu C
  • return ?value? ;# kann weitgehend vermieden werden
  • break ;# aus while/foreach/for-Schleifen
  • catch {body} ?result? ;# f�ngt Fehler in body auf
  • error "deutliche Fehlerbeschreibung"

Die wichtigsten Listen-Kommandos:

  • list el... ;# Anlage einer Liste aus Einzelelementen
  • lappend listname el... ;# Anh�ngen weiterer Elemente
  • lindex list index ;# Extraktion eines Elements anhand Position
  • lrange list from to ;# Extraktion eines zusammenh�ngenden Bereichs
  • lsearch list term ;# Suche eines Elements in einer Liste
  • llength list ;# gibt die L�nge der Liste zur�ck
  • split string ?splitchars? ;# Zerlegt einen String an splitchars in Listenelemente
  • join list ?joinchars? ;# F�gt joinchars zwischen je zwei Listenelemente, erzeugt einen String

Die wichtigsten String-Kommandos:

  • string length/is/compare/first...
  • append varname string.. ;# verl�ngert den String in varname
  • regexp re string.. ?-> varname1 varname2..?
  • regsub re instring replacement outvarname ;# ersetze re in instring durch replacement, Ergebnis geht nach outvarname

Die wichtigsten I/O-Kommandos:

  • gets channel ?varname? ;# bis Zeilenende (exklusiv)
  • puts channel string ;# mit Zeilenvorschub
  • open filename ?mode? ;# gibt Channel-Id zur�ck
  • close channel
  • read channel ?howmuch? ;# ohne R�cksicht auf Zeilenenden

Tcl hat eine kleine, aber qualifizierte weltweite Anwendergemeinschaft, die vor allem auf folgenden �ffentlichen Wegen kommuniziert:


Beispiel: Mittelwert einer Zahlenliste - prozedural oder funktional

 proc mean {L} {
    set sum 0
    foreach element $L {
        set sum [expr {$sum+$element}]
    }
    return [expr {$sum/[llength $L]}]
 }
 #### or: ####
 proc mean L {expr double([join $L +]) / [llength $L]} ;# funktional

Die Elemente der Liste L werden mittels join durch "+"-Zeichen zu einem String verbunden. Dieser wird in expr ausgewertet, durch double in Gleitkommazahl gewandelt (um Integer-Division zu vermeiden) und durch die L�nge der Liste geteilt. Das Ergebnis von expr ist implizit der R�ckgabewert von mean.


Beispiel: deutsche Umlaute normieren

 proc noUmlaut s {string map {� AE � OE � UE � SS} $s}

string map wird mit einer Liste von alternierend von-nach und einem String aufgerufen nimmt Ersetzungen von von nach nach vor und gibt den ge�nderten String zur�ck, der auch R�ckgabewert von noUmlaut ist.


Beispiel: Zuf�lliges Ziehen ohne Zur�cklegen aus einer Liste

 proc ldraw {_L} {
   upvar 1 $_L L
   set pos [expr {int(rand()*[llength $L])}]
   set res [lindex $L $pos]
   set L [lreplace $L $pos $pos]
   set res
 }

Die rand-Funktion von expr liefert eine Zufallszahl zwischen 0 und <1, die mit der Listenl�nge multipliziert und abgerundet einen g�ltigen Listenindex pos ergibt. Das dortige Element wird geholt und mit lreplace aus der Liste entfernt, die deshalb �ber Name und upvar �bergeben werden mu�.


Beispiel: Spiegeln einer Liste

 proc lrevert L {
    set i [llength $L]
    set res {}
    while {$i} {lappend res [lindex $L [incr $i -1]]}
    set res
 }
 % lrevert {foo bar grill test}
 test grill bar foo

Die Schleife �ber die Elemente der Liste beginnt mit N-1 (N als L�nge der Liste), da String- und Listenindizes ab 0 z�hlen, und endet mit 0. Die entsprechenden Listenelemente werden mit lindex geholt und mit lappend an die Ergebnisliste res geh�ngt. set res ist als letztes Kommando einer proc �quivalent zu return $res.


Beispiel: Introspektion des eigenen Proc-Namens

 proc myProcName {} {lindex [info level -1] 0}

info level gibt Zugriff auf den Aufrufstapel (callstack), info level -1 das Kommando, mit der der Aufrufer dieser Prozedur aufgerufen wurde. Das erste Element des Kommandos (lindex ... 0) ist definitionsgem�� der Name.


Beispiel: Zahlengenerator Diese Prozedur gibt bei jedem Aufruf eine andere Zahl zur�ck, aufsteigend 1,2,3,.... Bei jedem Aufruf definiert sie sich neu, wobei der Body unver�ndert bleibt (sie "schaut in sich selbst", aber der Defaultwert f�r den optionalen Parameter seed inkrementiert wird.

 proc intgen {{seed 0}} {
      proc intgen "{seed [incr seed]}" [info body intgen]
      set seed
 }

Beispiel: Konstanthalter Die Variable name wird auf den Wert value gesetzt, und zwar im Skopus des Aufrufers (uplevel 1). Dann wird ebenfalls dort ein Variablentrace gesetzt, der bei schreibenden Zugriffen auf name den urspr�nglichen Wert zur�cksetzt:

 proc const {name value} {
   uplevel 1 [list set $name $value]
   uplevel 1 [list trace var $name w "set $name [list $value];#" ]
 }
 % const Pi 3.14159
 % set Pi 4
 3.14159

Beispiel: �berladen eines Kommandos Jedes Tcl-Kommando, auch das elementarste set, kann �berladen werden, um z.B. Zusatzfunktionalit�t hinzuzuf�gen (etwa auf stdout die Aktivit�t anzuzeigen). Dazu wird das Original zun�chst umbenannt, um es nicht zu verlieren:

 rename set _set
 proc set {name args} {
    if [info level]>1 {puts -nonewline [info level -1]:}
    puts [info level 0]
    uplevel 1 _set $name $args
 }

So entsteht mit wenig Aufwand Debug-Funktionalit�t, die sich wieder ausschalten l��t mit

 rename set _; rename _set set; rename _ "" ;# l�schen

Beispiel: Klagloser Incrementor Das Tcl-Kommando incr leistet schnelle Integer-Addition bzw. Subtraktion, vorausgesetzt die Variable existiert bereits. Damit sie im anderen Fall automatisch angelegt wird (wie bei awk �blich), faktorisieren wir die sonst erforderliche Existenzpr�fung an diese proc aus:

 proc inc {varname {amount 1}} {
    upvar 1 $varname var
    if {![info exist var]} {set var 0}
    incr var $amount
 }

Hier wird die lokale Variable var an den in varname �bergebenen Namen einer (m�glichen) Variablen (kann auch Array-Element sein) im Skopus des Aufrufers gebunden. Damit kann var gepr�ft, intialisiert, inkrementiert werden.. und f�r den Aufrufer ist das gleiche mit $varname geschehen.


Beispiel: Zeichenh�ufigkeit

 proc charFreq string {
    foreach char [split $string ""] {
       inc t($char)
    }
    foreach {char count} [array get t] {
       lappend pairs [list $char $count]
    }
    lsort -integer -decreasing -index 1 $pairs
 }

Der string wird in Einzelzeichen gesplittet, �ber die iteriert und ein Element in dem tempor�ren Array t klaglos inkrementiert wird. Der Inhalt des Arrays, eine ungeordnete flache Liste von alternierend Zeichen und Anzahl, wird in eine Liste von Paaren gewandelt, die dann bequem nach absteigender H�ufigkeit sortiert werden kann. String kann Megabytes lang und nat�rlich auch koreanisch, griechisch oder arabisch sein...

 % charFreq Tennessee
 {e 4} {n 2} {s 2} {T 1}

Beispiel: Fehlertolerantes Lesen einer Textdatei

 proc readfile {filename} {
    if ![catch {open $filename} fp] {
        set res [read $fp [file size $filename]]
        close $fp
    } else {set res {}}
    return $res 
 }

Wenn das �ffnen der Datei filename gelingt (der catch nicht anschl�gt), wird sie in ganzer L�nge in die Variable res gelesen und zur�ckgegeben. Andernfalls wird ein leerer String zur�ckgegeben. M�gliche Anwendung als Zeilenz�hler:

 proc wc-l filename {llength [split [readfile $filename] \n] }

Beispiel: Textausgabe in Datei, mit Sicherheitskopie

 proc text2file {text filename} {
    if [file exists $filename] {
        file rename -force $filename $filename.bak
    }
    set    fp [open $filename w]
    puts  $fp $text
    close $fp
 }

Existiert eine Datei namens filename, so wird sie in (filename).bak umbenannt. Dann wird eine neue Datei des Namens zum Schreiben ge�ffnet, der �bergebene String (kann beliebig lang sein) mit abschlie�endem Zeilenvorschub hineingeschrieben, und die Datei geschlossen.


Beispiel: Internet-Dateiherunterlader

 package require http
 http::config -proxyhost proxy -proxyport 80
 puts [http::data [http::geturl [lindex $argv 0]]]

Verwendet wird das mit Tcl mitgelieferte package http. Mit http::config wird der hier �bliche Proxy-Rechner eingestellt. Die bei Aufruf des Scripts angegebene URL wird geladen und ihr Datenanteil (der eigentliche Inhalt) auf stdout ausgegeben, kann jedoch in eine Datei umgeleitet werden:

 $ tclsh ~/tcl/wwwget.tcl www.bahn.de/img/agb.gif > t.gif

Beispiel: ein kleines Zeichenprogramm

 proc doodle {w {color black}} {
    bind $w <1>         [list doodle'start %W %x %y $color]
    bind $w <B1-Motion> {doodle'move %W %x %y}
 }
 proc doodle'start {w x y color} {
        set ::_id [$w create line $x $y $x $y -fill $color]
 }
 proc doodle'move {w x y} {
        eval $w coords $::_id [concat [$w coords $::_id] $x $y]
 }
 pack [canvas .c]
 doodle       .c

F�r den Canvas werden Bindungen f�r "Mausklick links" (neue Linie beginnen, deren Anfang und Ende zusammenfallen: doodle'start) und "Mausbewegung mit gedr�ckter linker Taste" (aktuelle Linie, durch globale Variable ::_id zug�nglich, zum Zielort der Bewegung verl�ngern: doodle'move) definiert.


Beispiel: Digitaluhr in 6 Zeilen

 proc every {ms body} {
     eval $body
     after $ms [list every $ms $body]
 }
 pack [label .clock -textvar time]
 every 1000 {set ::time [clock format [clock sec] -format %H:%M:%S]}

Zun�chst wird ein einfacher, aber wiederverwendbarer Timer, every, definiert, der den �bergebenen body (Tcl-Kommandos) ausf�hrt und anschlie�end veranla�t, da� er nach ms Millisekunden erneut aufgerufen wird. Dies ist keine Rekursion! UI: Ein Label wird angelegt, an die Textvariable time gebunden, und in das Hauptfenster "gepackt". Mit every 1000 ... wird der Wert von time sek�ndlich auf die aktuelle Zeit gebracht und automatisch im Label angezeigt.


Beispiel: Analoguhr in etwas mehr Zeilen

 proc drawhands w {
     $w delete hands
     set secSinceMidnight [expr {[clock seconds] - [clock scan 00:00:00]}]
     foreach divisor {60 3600 43200} length {45 40 30} width {1 3 7} {
         set angle [expr {$secSinceMidnight * 6.283185 / $divisor}]
         set x [expr {50 + $length * sin($angle)}]
         set y [expr {50 - $length * cos($angle)}]
         $w create line 50 50 $x $y -width $width -tags hands
     }
 }
 pack [canvas .c -width 100 -height 100 -bg white]
 every 1000 {drawhands .c}

Ein canvas (Zeichenfl�che) mit 100*100 Pixeln Gr��e wird angelegt. Jede Sekunde werden die Uhrzeiger weggeworfen und als drei line-Items neu gezeichnet, wobei der Zielpunkt des Zeigers �ber Winkelfunktionen bestimmt wird. Man beachte die Mehr-Listen-Form von foreach, die in drei Durchl�ufen die drei Iterator-Variablen aus den konstanten Listen belegt. Der Timer every aus der Digitaluhr wird wiederverwendet.


Beispiel: eine umschaltbare Uhr Zuletzt eine Uhr, die durch Mausklick von analog zu Digital und zur�ck wechselt. Dazu ein wiederverwendbarer Umschalter, der das sichtbare von zwei Widgets unsichtbar (pack forget) und das andere sichtbar macht:

 proc toggle {w1 w2} {
     if [winfo ismapped $w2] {
         foreach {w2 w1} [list $w1 $w2] break ;# swap
     }
     pack forget $w1
     pack $w2
 }

Man beachte das Idiom zum Vertauschen zweier Variablenwerte (w1,w2) mit foreach. Schlie�lich legen wir die beiden Uhren an und setzen sie in Gang, "packen" zun�chst die Analoguhr und binden den linken Mausklick (<1>) im Hauptfenster (.) an die Umschaltung:

 canvas .analog -width 100 -height 100 -bg white
 every 1000 {drawhands .analog}
 label .digital -textvar time -font {Courier 24}
 every 1000 {set ::time [clock format [clock sec] -format %H:%M:%S]}
 pack .analog
 bind . <1> {toggle .analog .digital}

Diskussion: JCW - I'd like to make a few comments:

  • When you say everything is a string, please do mention that this is only at the conceptual level. Internally, things run at the speed of typed data.
  • The first example, Mittelwert/average, is of course very clever, but are you sure you want to bring this across as first example? How about pack [button -text "Hello world"]?
  • No mention of how trivial networking is? TclHttpd? AOL?

But above all... it's great to see Tcl summarized this way!


RS (1) fixed. (2) Added procedural counter-example. (3) Know too little... ;-)