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 ] See also Einfach man Tcl - Tcl/Tk: Programmieren auf dem PocketPC.
Tcl (Tool command Language, ausgesprochen "tickel") ist eine einfache, aber maechtige Programmiersprache. Da Turing-vollstaendig, kann im Prinzip jede Aufgabe in Tcl geloest 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:
Tcl kann mit C/C++/Java-Code fuer laufzeitkritische Teile auf verschiedene Weisen verbunden werden:
Tk (ToolKit) ist die wichtigste Tcl-Erweiterung, ein plattformunabhaengiger GUI-Toolkit (X11, Windows, Macintosh):
Weitere bekannte Erweiterungen:
Tcl bietet starke Unterstuetzung von Internationalisierung mit Unicode:
Tcl vereint Konzepte aus verschiedenen Software-Welten:
Tcl hat die einfachste Syntax aller Scriptingsprachen:
Kommandos haben folgende Eigenschaften:
Strukturierung von Tcl-Applikationen:
Datenhaltung in Tcl:
Ablaufsteuerung in Tcl:
Die wichtigsten Kommandos:
Die wichtigsten Listen-Kommandos:
Die wichtigsten String-Kommandos:
Die wichtigsten I/O-Kommandos:
Welche Probleme gibt es in Tcl nicht?
Tcl hat eine kleine, aber qualifizierte weltweite Anwendergemeinschaft, die vor allem auf folgenden oeffentlichen 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 Laenge der Liste geteilt. Das Ergebnis von expr ist implizit der Rueckgabewert von mean.
Beispiel: deutsche Umlaute normieren
proc noUmlaut s {string map {Ä AE Ö OE Ü UE ß SS ä ae ö oe ü ue} $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 geaenderten String zurueck, der auch Rueckgabewert von noUmlaut ist.
Beispiel: Zufaelliges Ziehen ohne Zuruecklegen 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 Listenlaenge multipliziert und abgerundet einen gueltigen Listenindex pos ergibt. Das dortige Element wird geholt und mit lreplace aus der Liste entfernt, die deshalb ueber Name und upvar uebergeben werden muss.
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 ueber die Elemente der Liste beginnt mit N-1 (N als Laenge der Liste), da String- und Listenindizes ab 0 zaehlen, und endet mit 0. Die entsprechenden Listenelemente werden mit lindex geholt und mit lappend an die Ergebnisliste res gehaengt. set res ist als letztes Kommando einer proc aequivalent zu return $res.
roho Statt "incr $i -1" muss es "incr i -1" heissen hae 2008.09.18: korrigiert; Beispiel funktioniert jetzt
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 definitionsgemaess der Name.
Beispiel: Zahlengenerator Diese Prozedur gibt bei jedem Aufruf eine andere Zahl zurueck, aufsteigend 1,2,3,.... Bei jedem Aufruf definiert sie sich neu, wobei der Body unveraendert bleibt (sie "schaut in sich selbst", aber der Defaultwert fuer 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 urspruenglichen Wert zuruecksetzt:
proc const {name value} { uplevel 1 [list set $name $value] uplevel 1 [list trace add variable $name write "set $name [list $value];#" ] } % const Pi 3.14159 % set Pi 4 3.14159
Beispiel: Ueberladen eines Kommandos Jedes Tcl-Kommando, auch das elementarste set, kann ueberladen werden, um z.B. Zusatzfunktionalitaet hinzuzufuegen (etwa auf stdout die Aktivitaet anzuzeigen). Dazu wird das Original zunaechst 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-Funktionalitaet, die sich wieder ausschalten laesst mit
rename set _; rename _set set; rename _ "" ;# loeschen
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 ueblich), faktorisieren wir die sonst erforderliche Existenzpruefung 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 uebergebenen Namen einer (moeglichen) Variablen (kann auch Array-Element sein) im Skopus des Aufrufers gebunden. Damit kann var geprueft, intialisiert, inkrementiert werden.. und fuer den Aufrufer ist das gleiche mit $varname geschehen.
Beispiel: Zeichenhaeufigkeit
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, ueber die iteriert und ein Element in dem temporaeren 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 Haeufigkeit sortiert werden kann. String kann Megabytes lang und natuerlich 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 Oeffnen der Datei filename gelingt (der catch nicht anschlaegt), wird sie in ganzer Laenge in die Variable res gelesen und zurueckgegeben. Andernfalls wird ein leerer String zurueckgegeben. Moegliche Anwendung als Zeilenzaehler:
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 geoeffnet, der uebergebene String (kann beliebig lang sein) mit abschliessendem 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 uebliche 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
Fuer den Canvas werden Bindungen fuer "Mausklick links" (neue Linie beginnen, deren Anfang und Ende zusammenfallen: doodle'start) und "Mausbewegung mit gedrueckter linker Taste" (aktuelle Linie, durch globale Variable ::_id zugaenglich, zum Zielort der Bewegung verlaengern: 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]}
Zunaechst wird ein einfacher, aber wiederverwendbarer Timer, every, definiert, der den uebergebenen body (Tcl-Kommandos) ausfuehrt und anschliessend veranlasst, dass 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 sekuendlich 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 (Zeichenflaeche) mit 100*100 Pixeln Groesse wird angelegt. Jede Sekunde werden die Uhrzeiger weggeworfen und als drei line-Items neu gezeichnet, wobei der Zielpunkt des Zeigers ueber Winkelfunktionen bestimmt wird. Man beachte die Mehr-Listen-Form von foreach, die in drei Durchlaeufen 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 zurueck 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. Schliesslich legen wir die beiden Uhren an und setzen sie in Gang, "packen" zunaechst 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:
But above all... it's great to see Tcl summarized this way!
RS (1) fixed. (2) Added procedural counter-example. (3) Know too little... ;-)
Sorry, I messed up all Umlauts wdb: repaired, don't worry
MSW: but I do know how easy networking is. (back to german)
Beispiel: Ein einfacher Zeitserver
proc server_handle {chan client port} { puts stderr "Neue Verbindung von $client:$port" lappend ::clients $chan fconfigure $chan -blocking 0 -buffering line } set clients [list] every 1000 { foreach c $::clients { puts $c [clock format [clock seconds] -format %H:%M:%S] } } socket -server server_handle 15000 vwait banzai
Zuerst wird ein Kommando definiert, das immer dann aufgerufen wird, wenn auf einem socket neue Verbindungen anstehen. Die entsprechenden, benoetigten Daten werden dem Kommando uebergeben. Der server_handle haengt daraufhin den geoeffneten Socket an eine globale Liste; Jede Sekunde schickt der Server dann an alle sockets in dieser Liste einen Zeitstempel. Das vwait wird benoetigt um Tcl in die Eventloop zu versetzen, d.h. das tcl auf Ereignisse warten & reagieren kann.
Zu diesem Zeitpunkt laeuft der Server und ist bereit - man kann ihn ausprobieren mit z.B. telnet:
$ telnet localhost 15000 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. 23:11:10 23:11:11 ... usw.
Alternativ kann man natuerlich auch einen kleinen client bauen:
Beispiel: Eine Netzwerkuhr
proc get_time_net {server port lab} { set chan [socket $server $port] fconfigure $chan -buffering line fileevent $chan readable "trans_time from $chan to $lab" } proc trans_time {from chan to lab} { $lab configure -text [gets $chan] if {[eof $chan]} { $lab configure -text {Keine Verbindung zum Server} ; catch { close $chan } } } pack [set lab [label .l]] get_time_net localhost 15000 $lab
get_time_net oeffnet einen socket zu besagtem server/port, und richtet einen callback ein, der aufgerufen wird, sobald Daten auf dem socket anstehen. Der callback wiederum liest die Daten, und passt den Text des labels an. Hier braucht man kein vwait - das Beispiel benutzt die wish, welche automatisch in der eventloop ist.
Anmerkung: Der Server vertraegt es nicht, wenn ein client seine Verbindung wieder verliert. Um den Server vor solchen Problemen zu beschuetzen baut man...
Beispiel: Ein stabiler Zeitserver
proc server_handle {chan client port} { puts stderr "Neue Verbindung von $client:$port." lappend ::clients $chan fconfigure $chan -blocking 0 -buffering line fileevent $chan readable "client_handle $chan $client $port" } proc client_handle {chan client port} { gets $chan if {[eof $chan]} { catch { close $chan } set ::clients [lreplace $::clients [set idx [lsearch $::clients $chan]] $idx] puts stderr "Verbindung zu $client:$port geschlossen." } } set clients [list] every 1000 { foreach c $::clients { puts $c [clock format [clock seconds] -format %H:%M:%S] } } socket -server server_handle 15000 vwait banzai
Dieser Server liest alle Anfragen vom client, nur um sie wegzuschmeissen - und das tut er nur, um herauszufinden, ob die Verbindung zum Client unterbrochen ist (via eof) . Falls ja, loescht er ihn aus seiner Verbindungsliste.
Beispiel: Ein tcl server
rename exit __exit__ proc server_handle {chan client port} { append ::buf($client) [gets $chan] if {[info complete $::buf($client)]} { rename puts _puts; proc puts args "_puts $chan \[lindex \$args end\]" ;# damit alle ausgaben beim client landen proc exit args "catch { close $chan}" ;# damit der client sich, und nicht den Server mit 'exit' beendet catch {set reply [uplevel $::buf($client)]} reply rename puts {} ; rename _puts puts ; rename exit {} set ::buf($client) "" } else { puts -nonewline $chan ">> "; flush $chan } if {[catch {eof $chan}] || [eof $chan]} { catch { close $chan } set ::clients [lreplace $::clients [set idx [lsearch $::clients $chan]] $idx] puts stderr "Verbindung zu $client:$port geschlossen." } elseif {[info exists reply]} { puts -nonewline $chan " --> $reply\n% "; flush $chan } } set clients [list] socket -server server_handle 15000 vwait chaos
Ein komplexeres Beispiel - der Server liest Anfragen von den clients, und prueft (mit info complete) ob diese Anfragen vollstaendige tcl Kommandos sind. Falls ja, fuehrt er sie aus, faengt etwaige Fehler (catch) ab, und schickt die Antwort an den client - wobei er auch gleich prueft ob die Verbindung zum client noch besteht. Die Funktion exit, die normalerweise den Server beenden wuerde, beendet nun den client; puts wurde geaendert (via rename) , automatisch nur auf den socket zu schreiben, und eventuelle vorherigen argumente (wie einen anderen channel oder ein -nonewline) zu ignorieren.
Wie gehabt, der Server laeuft zu diesem Zeitpunkt, und kann mit z.B. telnet getestet werden.
Wie man an diesen Beispielen sehen kann, ist die Kommunikation ueber sockets (fast) genauso einfach wie ueber stdout oder in Dateien. Die gezeigten Beispiele sind jedoch sehr low-level, es gibt zahlreiche Bibliotheken, mit denen man auch auf einem hoeheren Niveau uebers Netzwerk kommunizieren kann.
MSW: Noch ein Kommentar zu den vorgestellten Erweiterungen: Viele Tcl Programmierer (die ich kenne) kennen Tk schon aus scheme (z.B. aus STK), und suchen in Tcl oft nach einer CLOS/STKlos aehnlichen Objektumgebung. Otcl als CLOS-like OO Erweiterung zu erwaehnen (nach incr tcl) waer glaub ich noch fein :)
maidquellrasen - 2016-01-26 16:03:14
Muesste beim for-Kommando nicht statt
for {set i 0} {$i<$max} {incr i} {...} ;# analog zu C
besser mit expr verglichen werden, weil "3">"11" wenn ich Strings betrachte?
for {set i 0} {expr {$i<$max}} {incr i} {...} ;# analog zu C