This page was originally presented as 'Introduction to Tcl/Tk', in 2002. The original format was produced with LaTeX and is still available intro_tcl.pdf
This white paper is intended to be an short introduction to the Tcl/Tk scripting language. An attempt was made to include the most important Tcl concepts, pitfals, and frequently used programming constructs. There are several excellent books listed in the bibliography at the end of this reference.
I would recommend the following books in order:
tclsh is the Tcl shell interpreter. This interpreter does not include support for graphical Tk applications. It is executed by entering:
tclsh
Scripts are typically executed by entering:
tclsh program_name.tcl
Under Windows this program is named tclsh.exe or may contain a version number such as tclsh83.exe. Scripts can also be executed by entering the following command in the tclsh interpreter:
source program_name.tcl
wish is the Tcl/Tk interpreter. It supports Tcl commands as well as the graphical tool kit (Tk). It is executed by entering:
wish
Under Windows there is a program named wish.exe or may also contain a version name such as wish83.exe. Scripts can also be executed by entering:
wish program_name.tcl
Under Unix, Tcl applications can also be executed by entering in the filename at the command prompt. For this to work the Tcl filename must be referenced with an absolute path or be found in the current search path. The Tcl file must have execute privileges. The first line of the file should be:
#!/usr/bin/tclsh
Similarly for Tcl/Tk executing under the wish interpreter:
#!/usr/bin/wish
The manpage (man) command is supported by the tclsh interpreter. Be aware that command not built into the tclsh interpreter will be passed to the operating system for execution, so system manpages may be displayed.
$ tclsh
A truncated example manpage follows:
% man fconfigure fconfigure(n) Tcl Built-In Commands fconfigure(n) _________________________________________________________________ NAME fconfigure - Set and get options on a channel SYNOPSIS fconfigure channelId fconfigure channelId name fconfigure channelId name value ?name value ...? _________________________________________________________________ DESCRIPTION The fconfigure command sets and retrieves options for channels. ChannelId identifies the channel for which to set or query an option. If no name or value arguments are supplied, the command returns a list containing alternat ing option names and values for the channel. If name is supplied but no value then the command returns the current value of the given option. If one or more pairs of name and value are supplied, the command sets each of the named options to the corresponding value; in this case the return value is an empty string.
It should also be noted that this automatic passing of non Tcl commands to the operating system can be disabled by setting the auto noexec variable.
Comments are line based in Tcl. End of line comments are allowed but a semicolon MUST be placed at the end of the interpreted code on that line.
#!/usr/bin/tclsh # tcl is line comment based # no need to use semicolons for line terminations # but they are allowed set real_years 4 # this is a malformed comment (no semicolon) set dog_name "spot"; # semicolon is require for EOL commment set dog_years [expr (7 * $real_years)] puts "my dog $dog_name is $real_years" puts "he acts $dog_years old"
Line continuation is accomplished by using a backslash. Do not follow the backslash with any characters, or even any whitespace characters! The backslash escapes the next character which is intended to be the newline.
#!/usr/bin/tclsh
# the backslash escapes the new line
# dont put a space after it!
set members { \
george \
fred \
jeff \
alice \
}
puts $members
Curly braces around a string do not allow variable substitution within the string, the string is taken literally. Unmatched curly braces may cause the interpreter to wait indefinitely for more input, no errors, no warnings, no output. Curly braces count even if they are in a comment, this will cause brace mismatches that can be difficult to find.
Square brackets are used for controlling the order of execution.
Double quotes will allow substitution in the string being quoted. Curly braces will not allow substitution.
#!/usr/bin/tclsh
set years 41
set years_sub "I am $years";
set years_no_sub {I am $years};
puts "this string got substituted - weak quoting"
puts $years_sub
puts "this string did not substitute - rigid quoting"
puts $years_no_sub
The interpreter is told to perform math functions by using the expr command.
#!/usr/bin/tclsh
set r 2.0
set pi [expr { 4 * atan(1.0) }]
set area [expr {$pi * $r * $r}]
puts "r: $r"
puts "pi: $pi"
puts "area: $area"
Numbers with a leading zero, like 0644, are interpreted as octal. This is typically encountered when setting permission modes during the opening a file for writing. The leading zero may cause problems with the Tcl interpreter, when doing comparisons or assigments on numbers representing time of day. A digit that is greater than or equal to 8 preceeded by a leading zero digit will cause an illegal octal number error.
Variables are initialized with the set command. Variable are typeless strings but it is important how they are initialized. Initialization with an integer (no decimal) or a real value (decimal) will produce different results. Variables with leading zeros are assumed to be in an octal format. Variables are set without a dollar sign preceding the variable name. Variables are referenced with a dollar sign preceding the variable name. This is in contrast to other scripting languages like Perl that uses dollar signs for both initialization and referencing.
#!/usr/bin/tclsh set four 4 set four_real 4.0 set three 3 set threefourths [expr $three / $four] puts "threefourths: $threefourths" set threefourths_real [expr $three / $four_real] puts "threefourths_real: $threefourths_real"
Global variables are declared in procedures, not up front. Non-global variables within a procedure are local to the procedure. The info command can be used to determine if a variable exists or not.
There is a rich set of string processing commands in Tcl. The table below is not an exhaustive list but it does contain the most commonly used string commands.
Tcl/Tk String Command Summary
| Command | Description |
|---|---|
| string bytelength str | returns number of bytes used in for string storage |
| string compare str1 str2 | compares string and returns -1, 0 or 1 |
| string equal str1 str2 | test equality of string and returns 1 if equal |
| string first str1 str2 | returns the index of the first occurrence of str2 in str1, else returns -1 |
| string last str1 str2 | returns the index of the last occurrence of str2 in str1, else returns -1 |
| string length str | returns the length of str |
| string match pattern str | returns 1 if str matches pattern |
| string range str start end | returns substring of str from start to end |
| string repeat str n | returns a str repeated n times |
| string tolower str | returns a lowercased str |
| string totitle str | returns str with the first letter capitalized |
| string toupper str | returns a uppercased str |
| string trim str | trims whitespace from str |
| string trimleft str | trims whitespace from the end of str |
| string trimright str | trims whitespace from the beginning of str |
Many of these string commands have additional options such as nocase, positions, or non-default characters. See the respective manpages a more detailed description.
An example using the string command syntax follows:
#!/usr/bin/tclsh set str1 "th" set str2 "Is this supposed to be a good thing, Miri?" puts "length: [string bytelength $str2]" puts "first: [string first $str1 $str2]" puts "last: [string last $str1 $str2]" puts "toupper: [string toupper $str2]"
Lists are a sequence of values separated by whitespace. Braces are typically used to group items together into a single value in a list. Lists are not used to build complex data structures. Tcl lists are strings with a corresponding set of data manipulation commands. A incomplete summary of these commands are listed below.
Tcl/Tk List Command Summary
| Command | Description |
|---|---|
| list arg1 arg2 ... | constructs a list out of several arguments |
| lindex list n | returns the indexed element from the list |
| llength list | returns the number of elements in a list |
| lrange list n m | returns the range of elements of a list |
| lappend list arg1 arg2 ... | appends elements to a list |
| linsert list index arg1 arg2 ... | iserts elements to a list and returns a new list |
| lreplace list n m arg1 arg2 ... | replaces elements n thru m with args and returns a new list |
| lsearch list value | returns index of value in list, else -1 |
| lsort list | returns a sorted list |
Many of these list commands also have additional options. An example using the list command syntax follows:
#!/usr/bin/tclsh
set the_list { {Red Hat} SuSE Mandrake Debian }
puts "value 0: [lindex $the_list 0]"
puts "value 1: [lindex $the_list 1]"
puts "value 2: [lindex $the_list 2]"
puts "value 3: [lindex $the_list 3]"
set the_sorted_list [lsort $the_list]
puts "value 0: [lindex $the_sorted_list 0]"
puts "value 1: [lindex $the_sorted_list 1]"
puts "value 2: [lindex $the_sorted_list 2]"
puts "value 3: [lindex $the_sorted_list 3]"
Arrays in Tcl are associative, meaning that they are stored using a hash table. The array index to the associative array is placed within parenthesis. If the index is numeric the array syntax appears similar to other languages that do not use associative arrays. However, the indexes need not be numeric.
#!/usr/bin/tclsh
proc P_add_contact { key last_name email phone } {
global contact_last_name
global contact_email
global contact_phone
set contact_last_name($key) $last_name
set contact_email($key) $email
set contact_phone($key) $phone
puts "key: $key"
puts "name: $contact_last_name($key)"
puts "email: $contact_email($key)"
puts "phone: $contact_phone($key)"
return
}
P_add_contact 1000 aperson aperson@example.net 555-555-5555
P_add_contact 1001 bperson bperson@example.net 555-555-5556
P_add_contact 1002 cperson cperson@example.net 555-555-5557
Tcl is quite picky about the format of the if conditional statement. This is related to how the parser works. An example has been included to better illustrate the formatting of this conditional statement.
#!/usr/bin/tclsh
set a 1
# the if statement commented out causes a curly bracket mismatch
# if {$a == 1} {
if {$a == 0} {
puts "a is zero"
} else {
puts "a is not zero"
}
# need space between curly brackets
if {$a == 0}{
puts "a is zero"
} else {
puts "a is not zero"
}
# else is misplaced
if {$a == 0} {
puts "a is zero"
}
else {
puts "a is not zero"
}
# formatted properly
if {$a == 0} {
puts "a is zero"
} else {
puts "a is not zero"
}
# this is also formatted properly
if {$a == 0} {
puts "a is zero"
} else {
puts "a is not zero"
}
The syntax for loop control is covered in the next three examples.
for loop:
#!/usr/bin/tclsh
for { set loopcnt 0 } { $loopcnt < 10 } { incr loopcnt } {
puts "loopcnt: $loopcnt"
}
foreach loop:
#!/usr/bin/tclsh
set member_list {
george 41
alice 37
fred 30
sally 34
dustin 23
}
foreach {name age} $member_list {
puts [format "name: %-20s age: %3d" $name $age]
}
while loop:
#!/usr/bin/tclsh
set random_int 0
while { 1 } {
# loop exit condition
if { $random_int == 1 } {
break
}
# loop processing
set random_float [expr (10 * rand())]
set random_int [expr (int($random_float))]
puts $random_int
}
Procedures must be declared before they are referenced. In a script containing many procedures main program execution may be located at the bottom of the file.
Procedures may contain optional pass variables and may also specify defaults for these variables.
Procedure names may overwrite built in commands. By defining an exit procedure the normal built in exit command will be modified. This is not recommended practice and some developers use naming conventions to avoid problems.
#!/usr/bin/tclsh
proc P_procedure1 { } {
global variable1
global variable2
puts "variable1: $variable1"
puts "variable2: $variable2"
set variable3 [expr ($variable1 + $variable2) ]
return $variable3
}
proc P_procedure2 { variable3 } {
global variable1
global variable2
puts "variable3: $variable3"
return
}
set variable1 1.0
set variable2 2.0
set temp [P_procedure1]
P_procedure2 $temp
Scripts can be subdivided into seperate sections by use of the source statement. This provides similar functionality to the "require" in Perl or "include" in the C language.
Sourcing other Tcl scripts from a main Tcl script provides a way to separate shared procedures, configuration data, environment data, and system dependent data.
#!/usr/bin/tclsh source ex_source_err.tcl P_msg_info "We can Tcl code common to several modules" P_msg_warn "This is a warning message" P_msg_err "This is an error message"
Below is a listing of the Tcl code that was contained in a different file and sourced.
#!/usr/bin/tclsh
proc P_msg_info { msg } {
puts "INFO: $msg"
}
proc P_msg_warn { msg } {
puts "WARN: $msg"
}
proc P_msg_err { msg } {
puts "ERR: $msg"
}
System calls are performed by the exec command. The following example shows the exec command used with the catch command. This is useful for running a system command and capturing the output from that command. These procedures determine the operating system and hostname of the machine running the script. The return values of these procedures can be used to make system and hostname dependent decisions allowing the same code to run on different platforms and hosts.
#!/usr/bin/tclsh
set system_cmd "ls"
set system_arg "-la"
catch { exec $system_cmd $system_arg } ls_data
puts $ls_data
As with many other languages Tcl can be passed arguments from the command line at the start of command execution.
#!/usr/bin/tclsh
proc P_ex_arg { v0 v1 v2 } {
puts "v0: $v0"
puts "v1: $v1"
puts "v2: $v2"
return
}
# check for the proper number of command line arguments
if { $argc == 3 } {
# print out the script name
puts "executing $argv0..."
# setup the command line arguments
set arg1 [lindex $argv 0]
set arg2 [lindex $argv 1]
set arg3 [lindex $argv 2]
# call the main routine
P_ex_arg $arg1 $arg2 $arg3
exit 0
} else {
# put out an error message
puts "usage is: ex_arg.tcl arg1 arg2 arg3"
# exit with error
exit 1
}
#!/usr/bin/tclsh
# determines operating system
proc P_determine_os { } {
# set the subroutine name
set sub_name {P_determine_os}
# give the user some information
puts "($sub_name) INFO: determining operating system"
# execute the system command and catch the output
catch { exec uname -sr } uname_return_data
# tell the user what uname returned
puts "($sub_name) INFO: uname -sr returned: $uname_return_data"
# format the output of uname
if { [string first "Linux" $uname_return_data] == 0 } {
set os_type linux
} elseif { [string first "CYGWIN_NT" $uname_return_data] == 0 } {
set os_type cygwin
} elseif { [string first "SunOS" $uname_return_data] == 0 } {
set os_type solaris
} else {
set os_type undefined
}
puts "($sub_name) INFO: os_type set to: $os_type"
return $os_type
}
# determine operating system using a system call
P_determine_os
exit
#!/usr/bin/tclsh
proc P_determine_hostname { } {
# set the subroutine name
set sub_name {P_determine_hostname}
# give the user some information
puts "($sub_name) INFO: determining hostname"
# do the system call
catch { exec hostname } hostname_return_data
# tell the user what uname returned
puts "($sub_name) INFO: hostname returned: $hostname_return_data"
# no translation
set hostname $hostname_return_data
# trim the hostname
set hostname [string trim $hostname]
# lowercase the hostname
set hostname [string tolower $hostname]
puts "($sub_name) INFO: hostname set to: $hostname"
return $hostname
}
# call the procedure
P_determine_hostname
exit
#!/usr/bin/tclsh
# sets the current_date and current_time global variables
proc P_set_time { } {
global current_date
global current_time
# set the subroutine name
set sub_name {P_set_time}
# format time as YYYYMMDD
set current_date [clock format [clock seconds] -format "%Y%m%d"]
# format the time as HHMM
set current_time [clock format [clock seconds] -format "%H%M"]
# give the user some information
puts "($sub_name) INFO: current date: $current_date"
puts "($sub_name) INFO: current time: $current_time"
return
}
# call the procedure
P_set_time
exit
One of the benefits of Tcl and Tk is its ability to execute cross platform. File pathnames (slash, backslash, colon) and file formats (cr-lf combinations) have always been problematic to programmers. Tcl solves this problem by building up pathnames using the file join command.
#!/usr/bin/tclsh
# produces a text file
proc P_create_file { } {
# set the subroutine name
set sub_name "P_create_file"
# build up the file name
set abs_file_name [file join "/home" "gwhitema"]
set abs_file_name [file join $abs_file_name "tcl"]
set abs_file_name [file join $abs_file_name "training"]
set abs_file_name [file join $abs_file_name "ex_file_join.txt"]
# tell the user what is happening
puts "($sub_name) INFO: creating file: $abs_file_name"
# open the html file
set file_id [open $abs_file_name w 0644]
# write some text to the file
puts $file_id "this is some text"
# close the file
close $file_id
return
}
# call the procedure
P_create_file
exit
Reading data from a file line by line is a common task in programming. The following example details how to open a file for reading and split the data in the file on the new line.
#!/usr/bin/tclsh
proc P_read_file { file_name } {
# open the file
set file_id [open $file_name r]
# read in the file line by line
foreach line [split [read $file_id] \n] {
# process the line
puts $line
}
# close the file
close $file_id
return
}
# call the procedure
P_read_file "ex_file_read.txt"
exit
Writing data to a file is also a common programming task. The following examples details how to open a file for writing. Notice the leading zero in the octal file permission data field when the file is initially opened.
#!/usr/bin/tclsh
set file_name "ex_file_write.txt"
# open the dst file for write
set file_id [ open $file_name w 0644 ]
# put some data in the file
for { set loopcnt 0 } { $loopcnt < 10 } { incr loopcnt } {
puts $file_id "loopcnt: $loopcnt"
}
# close the file
close $file_id
Tcl can also produce file formats targetted for one OS while executing on a different OS. This is accomplished by usign the fconfigure command.
#!/usr/bin/tclsh
# produces a text file
proc P_create_file_fconfigure { } {
# set the subroutine name
set sub_name "P_create_file_fconfigure"
# build up the file name
set abs_file_name [file join "/home" "gwhitema"]
set abs_file_name [file join $abs_file_name "tcl"]
set abs_file_name [file join $abs_file_name "training"]
set abs_file_name [file join $abs_file_name "ex_fconfigure.txt"]
# tell the user what is happening
puts "($sub_name) INFO: creating file: $abs_file_name"
# open the file
set file_id [open $abs_file_name w 0644]
# generate using unix line terminations
# fconfigure $file_id -translation lf
# generate using dos line terminations
fconfigure $file_id -translation crlf
# generate using mac line terminations
# fconfigure $file_id -translation cr
# write some text to the file
puts $file_id "this is line 1"
puts $file_id "this is line 2"
puts $file_id "this is line 3"
# close the file
close $file_id
return
}
# call the procedure
P_create_file_fconfigure
exit
Regular expressions are supported through the use of the regexp statement. The following example reads in a file and strips comments indicated by a # sign in the first column.
#!/usr/bin/tclsh
# setup the filename
set file_name ex_regexp_in.txt
# open the file for reading
set file_id [open $file_name r]
# read in the file line by line
foreach line [split [read $file_id] \n] {
# process the line
if { [regexp "(ˆ#)" $line match] == 0 } {
puts $line
}
}
# close the file
close $file_id
This is the file used as input to the regexp example above.
# this is comment 1 cmd1 # this is comment 2 cmd2 arg1 arg2 # this is comment 3 cmd3 arg1 arg2 arg3 # this is comment 4 cmd4 # this is comment 5 cmd5
String subsititutions are supported through the use of the regsub statement.
#!/usr/bin/tclsh
# set up the src filename
set src_file_name "input.txt"
# set up the dst filename
set dst_file_name "output.txt"
# open the src file for read
set src_file_id [ open $src_file_name ]
# open the dst file for write
set dst_file_id [ open $dst_file_name w ]
# read the src line by line
while {[gets $src_file_id line] >= 0} {
# put your from-to translation strings here
puts $dst_file_id [regsub -all {cool} $line {dorky} line]
}
# close both files
close $src_file_id
close $dst_file_id
Tk controls the construction of widgets using one or more geometry managers. Geometry managers control the resizing and location of widgets during script execution. Various combinations of geometry managers can be used within a single Tcl/Tk script.
The following table lists the basic types of widgets used by Tk. This is not a complete table nor does it contain the file dialog widgets available for use.
| Basic Tk Widget Summary | Widget Description |
|---|---|
| button | creates a button to execute a command |
| radiobutton | creates a set of buttons, 1 of n can be selected |
| checkbutton | creates a set of buttons, any number can be selected |
| entry | used for a single text input line |
| message | creates a message text window |
| label | used for labelling other widgets with read only text |
| scale | slider control that calls a routine when the slider position changes |
| menubutton | displays a scrolldown menu |
| menu | holds menu pulldown items |
| listbox | creates a list box that can be scrolled |
| scrollbar | creates a scrollbar that is used to control other widgets |
| frame | creates a frame to control the grouping of other widgets |
| toplevel | creates a top level window with controls from the window manager |
| text | creates a text window that can be edited |
| canvas | creates a graphic window that is used for drawing and image display |
#!/usr/bin/wish
# button command processor
proc P_process_button { button_cmd_string } {
puts $button_cmd_string
return
}
# set the font size (pts)
set font_size 24
# frames for columns
frame .c0
frame .c1
frame .c2
# button for upper case characters
button .c0.b0 -text "B0" \
-font "-family arial -weight bold -size $font_size" \
-background #8080e0 -command { P_process_button "button 0" }
button .c0.b1 -text "B1" \
-font "-family arial -weight bold -size $font_size" \
-background #8080e0 -command { P_process_button "button 1" }
button .c1.b0 -text "B2" \
-font "-family arial -weight bold -size $font_size" \
-background #8080e0 -command { P_process_button "button 2" }
button .c1.b1 -text "B3" \
-font "-family arial -weight bold -size $font_size" \
-background #8080e0 -command { P_process_button "button 3" }
button .c2.b0 -text "B4" \
-font "-family arial -weight bold -size $font_size" \
-background #8080e0 -command { P_process_button "button 5" }
button .c2.b1 -text "exit" \
-font "-family arial -weight bold -size $font_size" \
-background #ff0000 -command { exit }
# pack the buttons
pack .c0.b0 -side top -fill both -expand true
pack .c0.b1 -side top -fill both -expand true
pack .c1.b0 -side top -fill both -expand true
pack .c1.b1 -side top -fill both -expand true
pack .c2.b0 -side top -fill both -expand true
pack .c2.b1 -side top -fill both -expand true
# pack the column frames
pack .c0 -side left -fill both -expand true
pack .c1 -side left -fill both -expand true
pack .c2 -side left -fill both -expand true
#!/usr/bin/wish
proc P_process_button { } {
global current_string
global display_width
if { [string compare $current_string "OFF"] == 0 } {
set current_string "ON"
} else {
set current_string "OFF"
}
.c0.l0 configure -text $current_string -width $display_width
return
}
# display adjustments
tk scaling 1
set font_size 24
set display_width 10
set xpadding 10
set ypadding 10
# setup the current label string
set current_string "OFF"
# frame for column 0
frame .c0
# button
button .c0.b0 -text "toggle" \
-font "-family arial -weight bold -size $font_size" \
-background #808080 -command P_process_button
# label widget
label .c0.l0 -text $current_string \
-font "-family arial -weight bold -size $font_size" \
-width $display_width
# pack the label, button, and column
pack .c0.l0 -side top -fill both \
-expand true -padx $xpadding -pady $ypadding
pack .c0.b0 -side top -fill both
pack .c0
#!/usr/bin/wish
proc P_contact_entry { } {
frame .contact_frame -borderwidth 2 -relief raised
label .contact_frame.last_name_label -text "last name"
entry .contact_frame.last_name_entry -textvariable last_name -width 40
label .contact_frame.phone_label -text "phone"
entry .contact_frame.phone_entry -textvariable phone -width 20
label .contact_frame.email_label -text "email"
entry .contact_frame.email_entry -textvariable email -width 40
button .contact_frame.save_button -text "save" \
-command { P_save_contact $last_name $phone $email }
button .contact_frame.exit_button -text "exit" \
-command { exit }
pack .contact_frame.last_name_label -side top -fill x -expand true
pack .contact_frame.last_name_entry -side top -fill x -expand true
pack .contact_frame.phone_label -side top -fill x -expand true
pack .contact_frame.phone_entry -side top -fill x -expand true
pack .contact_frame.email_label -side top -fill x -expand true
pack .contact_frame.email_entry -side top -fill x -expand true
pack .contact_frame.save_button
pack .contact_frame.exit_button
pack .contact_frame -fill both -expand true
return
}
proc P_save_contact { last_name phone email } {
puts ""
puts " $last_name "
puts " $phone "
puts " $email "
puts " "
return;
}
P_contact_entry
#!/usr/bin/wish # title on main window wm title . "Text Widget Example" # text widget with y scroll bar text .txtwin -width 80 -height 20 -bg grey \ -yscrollcommand ".yscroll set" # scrollbar widget scrollbar .yscroll -command ".txtwin yview" # pack the text window pack .txtwin -side left -fill y # pack the scrollbar pack .yscroll -side right -fill y
#!/usr/bin/wish
proc P_menu { } {
global editor
frame .mbar -borderwidth 2 -relief raised
frame .msg -borderwidth 2 -relief raised
menubutton .mbar.file -text "file" -menu .mbar.file.menupull
menu .mbar.file.menupull
.mbar.file.menupull add command \
-label "Edit" -command { exec $editor }
.mbar.file.menupull add command \
-label "Exit" -command { exit }
text .msg.txtwin -width 80 -height 20 -bg grey \
-yscrollcommand ".msg.yscroll set"
# scrollbar widget
scrollbar .msg.yscroll -command ".msg.txtwin yview"
pack .mbar.file -side left
pack .msg.txtwin -side left -expand true -fill both
pack .msg.yscroll -side left -fill y
pack .mbar -side top -fill x
pack .msg -side top -fill both -expand true
return
}
set editor "gvim"
P_menu