|
w w w . a q u a m e n t u s . c o m |
What is Tcl?
This page
Hello, world
Absolute basics
Absolute Basic #1: everything is a string
Absolute Basic #2: all commands follow the same format
Basics
Calling procedures
Math / expr
Control
if
while
for
switch
Procedures
Variable arguments
Default argument values
Variable scoping
global
upvar
uplevel
Lists
list
split
join
lindex
llength
concat
foreach
lappend
linsert
lreplace
lset
lsearch
lsort
lrange
Strings
string match
string length
string index
string range
string compare
string first
string last
string wordend
string wordstart
string tolower
string toupper
string trim
string trimleft
string trimright
format
regexes
regexp
regsub
Hashes
array exists
array names
array size
array get
array set
array unset
hash iterating with foreach
arrays as parameters
multi-level arrays
Dictionaries
File I/O
open
close
gets
puts
read
read
File Reading Example
File Writing Example
seek
tell
flush
eof
reading/writing binary data
glob
file
(file attributes)
file atime
file mtime
file size
file exists
file executable
file isdirectory
file isfile
file readable
file owned
file writeable
file lstat
file readlink
file stat
file type
(file manipulation)
file copy
file delete
file mkdir
file rename
(file path manipulation)
file dirname
file extension
file rootname
file tail
Subprocesses
exec
open
pid
Subprocessing with interp
interp create
interp delete
interp eval
interp alias
introspection
info exists
info script
info level
info tclversion
info patchlevel
less useful "info" commands
info cmdcount
info commands
info functions
info globals
info locals
info procs
info vars
info args
info default
info body
subst
namespaces
namespace eval
namespace export
namespace import
namespace current
namespace delete
namespace ensemble
variable
Example
packaging with "source"
packaging with "package"
package provide
package require
pkg_mkIndex
Putting packages and namespaces together
eval
eval
system
cd
pwd
argv/argc
ENV settings
clock seconds
clock format
clock scan
exceptions (and other errors)
error
catch
return
Debugging
error messages
trace
profiling
time
sockets
What is Tcl?
Tcl ("tool control language") is an interpreted programming language whose
design priority was integration into point tools. To that end, it
employs minimalism to ensure its interpreters are tiny and easily integrated.
(Why would you put a programming language inside a point tool? Because it
provides a user control and interface mechanism that's more
sophisticated and customizable than, say, menus and commandline switches.)
Tcl's minimalism comes at a cost, though. Its requirement for limited
lexical complexity results in a language with precious little syntactic
sugar. Users find that some whitespace is irritatingly important (such as
the needed lack of a carriage return between a "}" and an "else"). Software
engineering is difficult when there's no type safety.
Does "minimal syntax" and "lack of variable typing" remind you of
any other languages? Indeed it should: Tcl is, in essence, the assembly
language of the scripting world. (Except it's not fast, and the parse
error messages are essentially math puzzles.)
This page
The real Tcl tutorial (at
tcl.tk)
is suboptimal for a few reasons. First, it seems to have been written for
people who need to write a Tcl interpreter, not so much for people who need
to write a Tcl program. Second, there are some inconsistencies (and some
actual errors), which is frustrating.
This web page has two purposes:
- to be a "second opinion" for anyone trying to pick up Tcl for the first time
- to be a good casual reference
Which is why it's called a "tutref".
For most commands, I do not list out every last detail you may
ever want to know. For more information on any Tcl command, the best place to go is
actually its man
page. For example, to get all the details on the
foreach
command:
% man -s n foreach
Hello, world
What is any programming-language tutorial without a "hello, world"
example to kick it off? But it can't just be "hello, world", it has to be
"hello, world" from Marvin, the depressed robot in
The Hitchhiker's Guide to the Galaxy:
puts "Hello, cruel world!"
Things to notice right away:
- you can print to stdout right away, without having to #include anything
and/or drag a namespace into local scope. I hate C++ sometimes.
- there's no semicolon ending the statement. (Though there could be)
- the very first word of Tcl that you see is an insult.
Absolute basics
Before I get into any syntax, there are some very core fundamentals that you
should understand about Tcl. These core fundamentals will hang over your head
through this entire tutorial, and through your entire Tcl-programming career.
Absolute Basic #1: everything is a string
All variable values are strings. Even numbers. Even arrays. Even
hashes. Even code blocks.
Further, since everything is a string, you can sometimes leave out the quote
delimiters (if the string has no whitespace). All of the
following commands set the variable myvar
to the string "hi"
(without quotes):
set myvar "hi"
set myvar {hi}
set myvar hi
(The difference between quotes and braces is that quotes allow escape
sequences, variable interpolation, and command interpolation...though you
can always disable them with escapes. Interestingly, both quotes and braces
allow you to escape an end-of-line in the middle of a string. This code:
set myvar "foo\
bar"
sets myvar
to "foo bar", with a single space between the words,
regardless of the indenting before 'bar'!)
(Also you'll notice there's no single-quote version. Tcl doesn't like single-quotes.)
Where this will be most immediately irritating to the new Tcl developer is
when you try to do math:
set var1 4
set var2 7
set var3 $var1+$var2 ;# 'var3' set to '4+7'
set var4 $var1 + $var2 ;# syntax error: 'set' expects two params, not 4
set var5 [expr $var1+$var2] ;# yay! 'var5' set to '11'
set var6 [expr $var1 + $var2] ;# yay! 'var6' set to '11'
Absolute Basic #2: all commands follow the same format
Everything you do in Tcl will look like the following:
commandname <arg>*
You've seen the set
command already, which (usually) takes two arguments.
But when I say "everything looks like this" I mean "everything". The if
command
looks like this. The for
command looks like this. Even the
syntax for creating namespaces and packages are Tcl commands that look like
this. There is precious little syntactic sugar in the Tcl language.
Basics
The first handy thing to know is that the interpreter binary is called
tclsh
, for "Tcl shell". Running it acts just like every other
interpreted-language binary:
- if you give it a file name, it executes the file
- if you don't, it executes whatever you give it on
stdin
So if you want to make a Tcl script just like a perl script, you would
have a header like the following in your file:
#!/usr/local/bin/tclsh
...
One curious thing about tclsh
is that there is no -v option to
tell you which version of Tcl you've got. You have to use a Tcl command
to get it:
puts [info patchlevel]
Calling procedures
The way to call a Tcl procedure depends on whether you're calling it directly
or if it's part of another procedure call. If you're calling it directly, it
follows the syntax you've already seen in the above set
examples:
commandname <arg>*
If it's part of another procedure call, then the syntax is to enclose the
sub-procedure invocation in square brackets. This code:
command1 [command2]
invokes command2
and feeds its return value as an argument
to an invocation of command1
.
Math / expr
Math is its own section because Tcl does not have normal infix operators.
Instead, you have to use the expr
function, but at least expr
does allow infix. expr
allows you to use all standard math operations,
logic operations, bitwise functions, and math functions. (See below for
the complete list.)
expr
is a function, but it's a good idea to always give its
parameters in braces, such as "expr {$i + 1}
" instead of
"expr $i + 1
".
Ostensibly that's for security, so that the user can't give $i
a value like "[runSomeNastyFunction]
" that will get inadvertantly
executed by expr
.
Syntactic sugar: expr
is called implicitly by the condition checks in
control statements such as if
, while
, and for
. Yay!
You can specify any numbers in decimal, octal (with a prefix of "0"),
or hex (with a prefix of "0x").
For completeness, here are all the operators you can give to expr
,
in precedence order:
1 | + | unary plus |
- | unary negate |
~ | bitwise negate |
! | logical negate |
2 | ** | exponent |
3 | * | mult |
/ | divide |
% | modulus |
4 | + | add |
- | subtract |
5 | << | bit shift left |
>> | bit shift right |
6 | < | less-than |
> | greater-than |
<= | less-than-or-equal |
>= | greater-than-or-equal |
7 | eq | string equal |
ne | string not-equal |
in | string-in check |
ni | string-not-in check |
8 | & | bit and |
9 | ^ | bit xor |
10 | | | bit or |
11 | && | logical and |
12 | || | logical or |
13 | ?: | ternary |
All logic checks return a 0 or 1.
Here are all the math functions you can call:
abs acos asin atan
atan2 bool ceil cos
cosh double entier exp
floor fmod hypot int
isqrt log log10 max
min pow rand round
sin sinh sqrt srand
tan tanh wide
While Tcl does not have truly boolean values, it gives you some syntactic sugar
to pretend. If an expression evaluates to the string values true
,
false
, yes
, or no
, then its value to
expr
will be the corresponding boolean value.
Control
if
if {<check>} then? {<block>} [elseif {<check>} then? {<block>}]* [else {<block>}]
Notice that the then keyword is actually optional.
if { $var < 5 } {
puts "value too small!"
} elseif { $var > 100 } {
puts "value too large!"
} else {
...
}
while
while {<test>} {<block>}
while
works exactly as you'd expect: while the <test>
condition evaluates to true, the <block> is executed.
while
allows both continue
and break
statements.
set a 0
while { $a < 10 } {
puts "a is now $a"
incr a
}
Also note the cool function incr
, which is as close as Tcl gets to "x++".
Given a variable name, it increments
that variable's value. It is shorthand for "set a [expr $a+1]
".
Note in particular, that you pass the name of the variable to incr
,
not its value, so there is no dollar-sign sigil. (More on pass-by-reference
later!)
for
for {<init>} {<test>} {<incr>} {<body>}
for
also works exactly as you'd expect: it executes the <init>
code, then checks the <test>. If it evaluates to true, it executes
the <body> code, followed by the <incr> code, and repeats
back up to the <test>.
Like while
, for
also allows continue
and break
statements in the body.
for { set a 0 } { $a < 10 } { incr a } {
puts "a is now $a"
}
switch
switch <value> { [<pattern> {<block>}]+ }
The switch
statement is almost exactly equivalent to a chain of if-else's. In
particular, the blocks do not fall over into the next block, like they do in C.
The last <pattern> of a switch
can be the string "default",
which acts as a catch-all if none of the other patterns matches.
switch
returns the value of the executed block (which is the value
of its last command -- don't try to use return
for that!), or else an
empty string if nothing was run.
There are two very similar forms to the switch command -- one uses braces to
look just like C's switch
, and the other doesn't. The version with
braces looks more normal, but the version without braces allows variables
in <pattern>s.
Examples:
% set a 5
5
% switch 5 {
$a { puts "A" }
5 { puts "B" }
}
B
% switch 5 "
$a { puts {A} }
5 { puts {B} }
"
A
%
Warning! switch
uses string comparisons! Always! To pretend to
do integer evals, use lots of exprs:
set a 5.000
switch [expr int($a)] {
5 { ... }
}
Procedures
proc <name> {<args>} {<block>}
Defines a procedure. You can call these newly-defined procedures
directly, like they were any other Tcl command.
The returned value of a proc
is either set by an explicit
return
, or is implicitly the value of the last command run in the proc.
Good style dictates you use return
. :)
proc my_add { a b } {
return [expr $a + $b]
}
..
set foo 7
set my_sum [my_add 5 $foo] ;# my_sum set to 12
Believe it or not, Tcl allows two pretty advanced features for procedures:
a variable number of arguments, and default argument values.
Variable arguments
The number of procedure arguments can be fixed or variable. For a fixed
number, the <args> block is just a list of local variable names.
For a variable number, it's still a list of local variable names, but make
the last element of <args> be
the special string "args
" -- inside your procedure, it will be
set to the list of everything else passed to it.
Default argument values
Procedure arguments can have default values, so that callers don't have
to specify them. In the <args> list, specify a variable as
"{name value}
" instead of just "name
", and its
default value will be value
. All the usual rules for
default function values apply: they all have to be at the end of the
argument list, and they are assigned by position.
You can have both default values and a variable number of arguments. It
behaves exactly the way you'd think it would -- defaulted variables get
assigned first, and if there are more arguments than parameters, it
sets args
to the overflow.
proc my_func { a {b 1} args } {
set res [expr $a+$b]
foreach i $args {
set res [expr $res+$i]
}
return $res
}
..
my_func 1 ;# returns 2
my_func 1 2 ;# returns 3
my_func 1 2 3 ;# returns 6
Variable scoping
Variable scoping in Tcl is a little tricky, because it's neither lexical
scoping (such as what C has) nor dynamic scoping (such as what perl kind
of has). By default you cannot access variables in any outer scopes, so,
bizarrely, entering a procedure immediately hides all the global variables.
To get to global variables, you should always prefix them with "::".
global
global <vars>
Declares that the given variables come from the global scope. They are then
directly accessible to the local code. This is sort of similar to C++'s
using
, even though that's for namespaces.
set foo "bar"
proc my_proc {} {
global foo
puts "foo = $foo"
}
upvar
upvar [<level>] <parent_context_var> <new_local_var>
Declares a new local variable to be a reference to a variable in a runtime
parent scope. (Not the lexical parent scope!!) Any time Tcl encounters
your new_local_var
in an interpolation, it will instead use
the specified parent_context_var
from the parent scope.
The thought should cross your brain that if the parent_context_var
is from a runtime scope and not a lexical scope, then you really have no idea what
to say for parent_context_var
because the function could be called
by anyone, who may or may not have a variable with a particular name. Indeed,
this is the bane of dynamic scoping. In Tcl, this is a hack for a very specific
situation -- it's used for implementing pass-by-reference semantics, and the
only way it works is if the callers tell you the name of the variable to use.
This is how incr
works, and here is exactly the only way it can be done:
proc incr { parent_var } {
upvar $parent_var local_var
set local_var [expr $local_var + 1]
}
..
set a 5
incr a
The optional <level> argument can either be a bare number to indicate
the number of contexts to go up, or a number with a sharp (#) prefix to indicate the
number of contexts to go down from global. (So, "1" means "go up one context",
which means "parent context"; "#0" means "go down one context from global", which
means "global context".) The global
function is really just a special case
of upvar
.
The following quote from the Tcl documentation indicates why you should try not
to use either upvar or, really, Tcl in general:
If you are using upvar with anything except #0 or 1, you are most
likely asking for trouble, unless you really know what you're doing.
uplevel
uplevel 1 <code>
This is essentially eval
of <code> but in the parent's context, so that it
has native access to all of
the parent's variables. This is needed for implementing things like while
.
def myloop { condition body } {
#while { $condition } { ;# fails because 'a' is unknown
while { [uplevel 1 $condition] } {
#eval $body ;# fails because 'count' is unknown
uplevel 1 $body
}
}
...
set count 0
myloop { a > 0 } {
set count [expr $count + $a]
decr a
}
(Note: I didn't test this example..I wrote this one away from a machine that
has tclsh..)
Lists
Remember how I said, in absolute basic #1, that everything in Tcl is just
a string? Well, lists are just strings with whitespace. Even commands
are really just lists, which are really just strings with whitespace.
The following quote comes directly from the Tcl tutorial:
Tcl commands often have subcommands. The "string" command is an example
of one of these. The "string" command treats its first argument as a
subcommand. Utilizing subcommands is a good way to make one command do
multiple things without using cryptic names. For instance, Tcl has
"string length" instead of, say, "slength".
Given that plausible argument for all the string-related commands, what do you
think we're going to do for all the list-related commands? If you said "we'll
have a main command called 'list' with subcommands for each of the functions
we need!", then you, my friend, are gonna hate this section. The Tcl designers
decided instead to implement all the list functions with "cryptic" names. Even
after saying how awesome it was that they chose "string length" instead of
"slength", the list length function is named "llength".
Great. Well, here we go!
list
list <arg>*
Creates a list from the given elements. This is just one way to create a list.
set my_list [list a b c d e] ;# my_list has 5 elements
set my_list {a b c d e} ;# exactly equivalent
set my_list "a b c d e" ;# also equivalent
split
split <string> <chars>?
Splits the given <string> wherever there are <chars>, which defaults
to whitespace characters. Note: <chars> is a list of characters, not
a string: split will split when it finds any one of the given characters!
set my_comma_separated_string "a,b,c,d,e"
set my_list [split $my_comma_separated_string ","]
join
join <list> <str>
Joins together each element of the <list> with a copy of <str>.
set mylist [list 1 2 3 4]
set as_string [join $mylist ", "] ;# "1, 2, 3, 4"
lindex
lindex <list> <index>
Returns the (index+1)'th element of the given list. (It's index+1 because the index starts at zero.)
set my_list [list a b c d e]
set fourth_element [lindex $my_list 3]
llength
llength <list>
Returns the number of elements in the given list.
set my_list [list a b c d e]
set list_len [llength $my_list]
concat
concat <arg>+
Joins all the given args into a single list. If the arg is a
list, its elements are separately added to the result. (i.e. it doesn't add
the list as a single object)
set my_list [concat [list a b c] d [list e f]] ;# creates 'a b c d e f'
foreach
foreach [<var>+ <list>]+ {<block>}
foreach
sounds like it should be in with all the other control
statements like for
and while
, but it turns out
that foreach
is very specific to iterating over lists.
As you can surmise, foreach
iterates over each element of the
given lists, and executes the given <block> for each value.
The easiest, most common way to use foreach
is with one index
variable and one list. However, you can also specify multiple index variables
per list (to let you walk through more than one element per iteration), as
well as multiple variables in multiple lists (to let you walk through more
than one list in parallel).
foreach j $list { ... }
foreach j $list1 k $list2 { ... }
foreach {j k} $list { ... }
But if you want to iterate through multiple lists serially, you have
to concat
the lists:
foreach j [concat $list1 $list2] {... }
lappend
lappend <listname> [&kt;arg>]+
Appends the given arg
s to the given listname
. List-type args
are not
flattened, so lists are appended as a single object. Also note that this
command takes a list NAME and not a LIST.
set a [list a b c]
set b [list 1 2 3]
lappend a $b ;# -> a is now 'a b c {1 2 3}'
linsert
linsert <list> <index> [<arg>]+
Inserts the given arg
s to the given list
starting at the given index
.
List-type arg
s are not flattened; lists are inserted as a single object.
Also: the special value "end
" can be used for the index to indicate that the
elements should be inserted at the end of the list, making it essentially the
same as calling lappend
.
set l [list a b c]
linsert l 1 d e ;# => returns 'a d e b c'
lreplace
lreplace <list> <first_index> <last_index> [<arg>]+
Replaces elements first_index
through last_index
(inclusive) with the
arg
s given. List-type arg
s are not flattened.
Note that this is how to remove things from lists, since there's no lremove
function.
set l [list a b c d]
lreplace l 1 2 e f g ;# => returns 'a e f g d'
lreplace l 0 1 {} ;# => returns with the first element removed
lset
lset <listname> <index> <value>
Sets the index
'th value of listname
to value
. List-type arg
s are not
flattened.
set l [list a b c]
lset l 1 d ;# => l is now 'a d c'
lsearch
lsearch <list> <glob_pattern>
Looks for the given glob_pattern
in list
, and returns the index of the first
element that matches. lsearch
uses glob patterns, not regexs, though there's a
switch if you want to use a regex pattern instead. If no match is found,
lsearch
returns -1.
set l [list a1 b2 c3]
set i [lsearch $l *2] ;# => i set to 1
lsort
lsort <list>
Sorts the given list
alphabetically, and returns the sorted version.
set l1 [list b d a c]
set l2 [lsort $l1] ;# => l2 set to 'a b c d'
lrange
lrange <list> <first_index> <last_index>
Returns the sub-list of list
from first_index
to last_index
(inclusive).
Once again you may use the special value "end
" to indicate the end of the
list. (There is no "first
", you would just use 0.)
set l1 [list a b c d]
set l2 [lrange $l1 1 2] ;# => l2 set to 'b c'
Strings
string match
string match <glob_pattern> <string>
Uses glob-type patterns to see if pattern
matches string
. Glob-type
patterns allow "?" (which matches any single character), "*" (which matches
any number of any kind of character), and "[]" (which match just the
characters listed in the brackets.
set str "the quick brown fox"
if { [string match "qu?ck" $str] } { ... } ;# => fails
if { [string match "*qu?ck*" $str] } { ... } ;# => succeeds
string length
string length <string>
Returns the length of the given string
.
set str "foo"
set len [string length $str] ;# => len set to 3
string index
string index <string> <index>
Returns the index
'th character of string
. You can use the special value
"end
".
set str "asdf"
set c1 [string index $str 1] ;# => c1 set to 's'
set c2 [string index $str end] ;# => c2 set to 'f'
string range
string range <string> <first_index> <last_index>
Returns a string consisting of the characters between first_index
and
last_index
(inclusive) of string
.
set str "asdf"
set substr [string range $str 1 2] ;# => substr set to 'sd'
string compare
string compare <string1> <string2>
Returns what C's strcmp would return: -1 if string1
ascii-sorts before
string2
, +1 if vice versa, and 0 if the strings are the exact same.
Note that you can use eq
and ne
as well for string comparisons. The
math operator ==
will actually work for strings as well,
as long as the two strings can't both be coerced into numbers.
set str1 "asdf"
set str2 "fdsa"
if { [string compare $str1 $str2] == 0 } { ... }
string first
string first <sub_string> <string>
Returns the index at which sub_string
first appears in string
, or -1 if
it doesn't.
set str "the quick brown fox"
set offset [string first "quick" $str] ;# => offset set to 4
string last
string last <sub_string> <string>
Returns the index at which sub_string
last appears in string
, or -1 if
it doesn't.
set str "foo bar bas"
set offset [string last "ba" $str] ;# => offset set to 8
string wordend
string wordend <string> <index>
Assuming the index
th character of string
is in the middle of a word, this
function returns the index of the end of that word. (Actually, the first
character past the end of the word.) "words" contain letters, numbers,
and underscores, and possibly also unspecified but specific kinds of punctuation
marks.
set str "the quick brown fox"
set offset [string wordend $str 5] ;# => offset set to 9
string wordstart
string wordstart <string> <index>
See the description for "string wordend"
set str "the quick brown fox"
set offset [string wordstart $str 5] ;# => offset set to 4
string tolower
string tolower <string>
Returns a fully lowercased version of string
.
set str "The Quick @#$@#%@ Fox!"
set lstr [string tolower $str]
string toupper
string toupper <string>
Returns a fully uppercased version of string
.
set str "The Quick @#$@#%@ Fox!"
set ustr [string toupper $str]
string trim
string trim <string> [<chars>]?
Returns string
but with all of the chars
stripped off the beginning and
ending. By default, chars
is whitespace.
set str " asdf "
set str [string trim $str] ;# => str is 'asdf'
string trimleft
string trimleft <string> [<chars>]?
Returns string
but with all of the chars
stripped off the beginning.
By default, chars
is whitespace.
set str " asdf "
set str [string trim $str] ;# => str is 'asdf '
string trimright
string trimright <string> [<chars>]?
Returns string
but with all of the chars
stripped off the end. By
default, chars
is whitespace.
set str " asdf "
set str [string trim $str] ;# => str is ' asdf'
format
format <format_str> [<arg>]+
format
is Tcl's equivalent of C's sprintf. You can use pretty much everything
you can with sprintf; the most common things you would need are:
- %s: string
- %d: (decimal) integer
- %o: (octal) integer
- %x: (hex) integer
- %f: floating-point number
You can left/right justify with -/+, and specify field width with a number, as usual.
set val1 42
set val2 18.2
set str "eek"
set nice_str [format "%+4s! %d != %f!" $str $val1 $val2] ;# nice_str set to ' eek! 42 != 18.2!"
See the documentation on sprintf for a lot more details. :)
regexes
Regular expressions ("regexs") are an immense topic, but fortunately
you can get 80% of their usefulness with just the basics.
regexp
regexp [<opts>] <pattern> <orig_str> [<output_full_match>] [<output_sub_matches>]*
Looks for the given pattern in str. Returns 0 or 1 to indicate if the
pattern was found or not. (Usually.)
Since the gold standard for regex specification is perl, here are several
examples contrasting common regex usage between it and Tcl:
basic match
perl: if ($str =~ /f.o/)
Tcl: if { [regexp {f.o} $str] }
capture of one element
perl: if ($str =~ /f(.o)/) { my $match = $1; ...
Tcl: if { [regexp {f(.o)} $str match] }
capture of two elements
perl: if ($str =~ /(ab)?cd(e.*)?f/) { my ($m1, $m2) = ($1, $2); ...
Tcl: if { [regexp {(ab)?cd(e.*)?f} $str full m1 m2] }
non-capture of parentheses
perl: if ($str =~ /f(?:.o)*/)
Tcl: if { [regexp {f(?:.o)*} $str] }
case-insensitive match
perl: if ($str =~ /f.o/i)
Tcl: if { [regexp -nocase {f.o} $str] }
return captured strings, instead of success flag
perl: my @matches = ($str =~ /f.o/g);
Tcl: set matches [regexp -inline -all {f.o} $str]
count the number of matches
perl: my @matches = ($str =~ /f.o/g); my $cnt = scalar @matches;
Tcl: set cnt [regexp -all {f.o} $str]
regsub
regsub [<opts>] <pattern> <str> <new_text> <res>
Substitutes string matches. Returns the number of substitutions it
made, and puts the resulting string into res
.
regsub -all {,} "a,b,c,d,e" ":" res ;# returns '4'
puts $res ;# prints "a:b:c:d:e"
regsub
is the Tcl equivalent to perl's s/// statement. They're not
exactly the same, because the Tcl version returns a new string instead
of modifying one in place. The approximate perl version of the above would be:
my $res = "a,b,c,d,e";
$res =~ s/,/:/g;
print "$res\n";
One horrific concern with regexs in Tcl is that even regex patterns are
just strings, so you have to be more careful than usual with escaping and
interpolation. It is always a good idea to put the pattern in braces, so
that Tcl doesn't try to interpolate brackets and dollar signs.
Hashes
Tcl prefers to call hashes "associative arrays", presumably because they
are quite adamant that efficiency is not one of their priorities. Like
for strings, there is one big command that has several sub-commands. The
big command is called, disappointingly, array
.
The syntax for array variables is to include the key name in
parentheses as part of the variable name. Example:
set foo(bar) "hi mom!"
set foo(asdf) "bye mom!"
Oddly, you cannot use braces around the full names of the variables:
set asdf ${foo(bar)} ;# no
set asdf $foo(bar) ;# yes
array exists
array exists <array_name>
Returns whether array_name
is an array variable. array_name
is the part
before the parentheses.
set foo(bar) "hi mom!"
if {[array exists foo]} {
puts "'foo' is a hash!"
}
This function does NOT tell you if a given key exists in an array!! For that,
use info exists
!!
if { [info exists foo(bar)] } {
puts "yes, foo has a 'bar' entry."
}
array names
array names <array_name>
Returns the list of key names that exist for the given array_name
.
set foo(bar) "asdf"
set foo(bas) "fdsa"
set keys [array names foo] ;# => keys is 'bar bas'
array size
array size <array_name>
Returns the number of elements in array_name
set foo(bar) "asdf"
set foo(bas) "fdsa"
set num_keys [array size foo] ;# => num_keys is '2'
array get
array get <array_name>
Returns a list that is constructed from the keys and values of
array_name
. The resulting string can be fed into array set
to get the
original array back.
This is useful for serializing arrays.
set foo(bar) "asdf"
set foo(bas) "fdsa"
set str [array get foo] ;# => str is 'bar asdf bas fdsa'
array set
array set <array_name> <list>
Deconstructs the given list
into key-value pairs, and assigns them into
the given array_name
. This function will overwrite existing settings in
array_name
that are defined in list
, but it will leave the others
alone. In that sense, it's a sort of bulk append.
set str 'bar asdf bas fdsa'
array set foo $str ;# => foo(bar) is 'asdf', and foo(bas) is 'fdsa'
array unset
array unset <array_name> [<pattern>]?
Deletes all the elements of the array_name
whose keys match pattern
.
By default, pattern
matches everything.
set foo(bar) "asdf"
array unset foo ;# => foo doesn't have 'bar' anymore
hash iterating with foreach
For iterating over a hash, you can use the sneaky multi-variable version
of foreach
along with the array serialization function:
foreach {key value} [array get my_array] { ... }
For example:
set foo(bar) "asdf"
set foo(bas) "fdsa"
foreach { this_key this_value } [array get foo] {
puts "$this_key is $this_value"
}
arrays as parameters
Tcl hashes have a nasty gotcha when passed as procedure parameters. Since
they have no value, they cannot be passed by value; therefore you have to
pass them by reference, which means passing a name and upvar'ing:
proc print12 {array} {
upvar $array a
puts "$a(1), $a(2)"
}
set array(1) "A"
set array(2) "B"
print12 array
multi-level arrays
Another drawback to arrays is that they are nonhierarchical -- you cannot
have a hash of a hash. The best you can do is to emulate it by including
a comma in the key name, like so:
set my_arr(foo,bar) 1
But note that all we do there is have a single key named "foo,bar". If you
introduce any whitespace around the comma, you're toast -- the string
"foo, bar" is lexigraphically different from "foo,bar", so it won't be
found.
Tcl for the win..
Dictionaries
Dictionaries are the next evolution of associative arrays. They serve the
exact same function (pretending to be a hash), except they stop sucking in
the following ways:
- they can be passed to procedures like regular variables
- they are hierarchical, so you can have a hash of hashes
The one big drawback: they're pretty recent. They only came out in Tcl
version 8.5, which was released in June 2011 and does not seem to be common yet.
Another drawback is that they have different syntax. Unfortunately I
can't give you a good description of dictionaries; the tutorial page is
almost useless, and I've got Tcl 8.4 so the man page doesn't exist. You
can check out
the tutorial
page for more info, if you dare.
File I/O
open
open <path> <mode> [<permissions>]?
Opens the given file and returns a file handle. mode
follows the C
convention: r, r+, w, w+, a, or a+. permissions
is only applicable
when creating a file; its default value is 0666 anyway.
(Full example below.)
close
close <file_handle>
Closes the file associated with file_handle
.
(Full example below.)
gets
gets <file_handle> [<var>]
Reads a line from file_handle
, discarding the newline. If var
is
specified, then the line is put into that variable, and gets
returns the
number of characters read (or -1 if EOF). If var
is not specified,
gets
returns the line read. An EOF will manifest as an empty string, but
so will any empty lines in the input, so if you use gets
this way you'll
have to use eof
to check for end-of-input.
(Full example below.)
puts
puts [-nonewline] <file_handle> <string>
Writes the given string
to the file_handle
, optionally with or without
a newline. For file_handle
you can also specify the special values
"stdout
" or "stderr
".
(Full example below.)
read
read [-nonewline] <file_handle>
Reads all of the rest of the file, returning it as a string.
-nonewline
will discard only the very last character if it happens
to be a newline.
(Full example below.)
read
read <file_handle> <num_bytes>
Reads the next num_bytes
of the file, returning it as a string.
(Full example below.)
File Reading Example
## all problems found by 'open' throw exceptions:
if {[catch {set read_fh [open "input.txt" "r"]} errmsg]} {
error "ERROR: $errmsg"
}
## read the file line-by-line:
while {[gets $read_fh this_line] != -1} {
...
}
## or read the entire file into one string:
set file_contents [read $read_fh]
## or read the next N bytes into a string:
set next_block [read $read_fh 1024]
close $read_fh
File Writing Example
## all problems found by 'open' throw exceptions:
if {[catch {set write_fh [open "output.txt" "w"]} errmsg]} {
error "ERROR: $errmsg"
}
puts $write_fh "blah blah blah"
## need a catch on close, because that's where most errors happen:
if {[catch { close $write_fh } errmsg]} {
error "ERROR: $errmsg"
}
seek
seek <file_handle> <offset> [<origin>]
Moves the position in the file to the given offset. origin
can be
the special strings "start
", "current
", or "end
".
# read just the last 256 bytes of the file:
seek $read_fh 256 end
set last_block [read $read_fh 256]
tell
tell <file_handle>
Returns the current file position.
set my_current_loc [tell $read_fh]
flush
flush <file_handle>
Flushes any buffers on the associated file_handle
.
flush $write_fh
eof
eof <file_handle>
Returns 1 if the given file_handle
has hit the end-of-file; otherwise
returns 0.
reading/writing binary data
All the functions listed so far were meant for dealing with text
files. Reading or writing binary data requires you to go through
fconfigure
.
Power tip: if you have control over your script's input format, you could make
it be Tcl source code. Then you can use
source
instead of having to open
files and parse them yourself!
glob
glob [-nocomplain] [-types <types>] <pattern>
Lists the files that match the glob-pattern pattern
. The glob-pattern
lets you use "." and "*" as usual, but also allows you to braces to
specify a sub-list. If you want files in another directory, you need to
specify the path as part of the pattern
. The -nocomplain
keeps glob
from throwing a fit if there don't happen to be any matches. The -type
switch lets you restrict the matches to files or directories, or certain
permissions. There are lots of things you can do with glob; check the
man page.
set file_list [glob diffs.{old,new}]
file
file
is another major command that has a ton of subcommands, mostly for
querying timestamps and status. Times are reported in seconds since
1/1/1970.
(file attributes)
file atime
file atime <path>
Returns the last access time of the given file.
set last_read_time [file atime "/usr/blah/blah"]
file mtime
file mtime <path>
Returns the last modification time of the given file.
set last_changed [file mtime "myinput.txt"]
file size
file size <path>
Returns the size of the file (in bytes).
set size [file size "myfile.txt"]
file exists
file exists <path>
Returns whether or not the given file exists and the user has read access
to all its parent directories.
if {![file exists "input.txt"]} {
...
}
file executable
file executable <path>
Returns whether or not the given file is executable.
if {[file executable "/usr/local/bin/perl"]} {
...
}
file isdirectory
file isdirectory <path>
Returns whether or not the given path is a directory.
if {![file isdirectory "indir"]} {
...
}
file isfile
file isfile <path>
Returns whether or not the given path is a regular file.
if {[file isfile "maybe_a_link.txt"]} {
...
}
file readable
file readable <path>
Returns whether the file is readable by the current owner.
if {![file readable "input.txt"]} {
...
}
file owned
file owned <path>
Returns whether or not the current user owns this file.
if {[file owned "input.txt"] ne $env(USER)} {
...
}
file writeable
file writeable <path>
Returns whether or not the current user can write the the path.
if {![file writeable "proposed.output.txt"]} {
...
}
file lstat
file lstat <path> <varname>
Runs the usual lstat function on the given path, and populates the given
varname
as an array variable with fields such as
atime, ctime, dev, gid, ino, mode
, etc.
file lstat "input.txt" file_info
if {file_info(mode) == 0666} {
...
}
file readlink
file readlink <path>
Returns the text content of a symlink.
set tgt [file readlink "link.txt"]
file stat
file stat <path> <varname>
Runs the stat
function on the given path, and populates the given
varname
as an array with various fields.
file stat "myfile.txt" file_info
if {file_info(atime) < ... } {
...
}
file type
file type <path>
Returns a string that describes what path
is. The string is one of
file, directory, characterSpecial, blockSpecial, fifo, link
, or
socket
.
if {[file type "myinput.txt"] ne "file"} {
...
}
(file manipulation)
file copy
file copy [-force] <source> <dest>
Copies the file. dest
may be a directory.
file copy "myfile.txt" "myfile.txt-backup"
file delete
file delete [-force] <dest>
Deletes the given file or directory. -force
lets you delete non-empty
directories.
file delete "goner.txt"
file mkdir
file mkdir <path>
Makes a new directory. The "-p" is implied, so it will create all necessary
parent directories.
file mkdir "newdir"
file rename
file rename [-force] <source> <dest>
Renames the given file/directory. -force
lets you overwrite an existing
file.
file rename "existing_output.txt" "out_of_the_way.txt"
(file path manipulation)
file dirname
file dirname <path>
Returns the directory portion of the given path
set dir [file dirname "/foo/bar/bas.txt"] ;# => dir is '/foo/bar'
file extension
file extension <path>
Returns the extension part of the given path (such as ".txt" or ".tcl").
set ext [file extension "/foo/bar/bas.txt"] ;# => ext is '.txt'
file rootname
file rootname <path>
Returns the root of the path, which is the part that isn't the extension.
set root [file rootname "/foo/bar/bas.txt"] ;# => root is '/foo/bar/bas'
file tail
file tail <path>
Returns the non-directory parts of the file path string.
set tail [file tail "/foo/bar/bas.txt"] ;# => tail is 'bas.txt'
Believe it or not, there are even more file
subcommands. Check the
man
page.
Subprocesses
There are two ways to run a sub-process from Tcl. The first is exec
,
which, despite its name, behaves more like what's usually called "system":
it runs things as a separate subprocess. The second is to use open
, which lets
you connect the sub-process's I/O to a file handle.
exec
exec [-keepnewline] <program> [<args>]*
Runs the given program
with the given args
, and returns either the
output or an error. You can redirect and pipe pretty much anything you
want, like you would with the shell, but Tcl gives you the cool new ability to
redirect stdin or stdout to or from Tcl file handles using
"<@" and ">@".
I highly recommend reading through the man page for exec
if you're not entirely comfortable with this hand-wavy discussion of
redirections. :)
Warning on exec: if the subprocess writes anything to stderr, exec
considers it to have failed, and it will then die! You have one of two
workarounds: first, call exec with -ignorestderr; second, use catch
.
(Well, you should probably use catch
any time you use exec
anyway.)
By default, exec
is blocking. You can push it into the background by
appending "&" to the very end of the command line; in that case, exec
returns the pids of the sub-processes (instead of the output).
# check no errors at all:
exec /bin/ls *
# catch errors:
if {[catch {exec /bin/ls *} output]} { ...
puts "ERROR: ls failed. Output: $output"
} else {
puts "Result of ls: $output"
}
open
open |<progname> [<access>]?
access
is technically optional because it defaults to "r", but when you
use it for connecting to a process I think it's a good idea to always
specify it.
To drive a sub-process's input, use "w":
# some problems (missing program) happen on open():
if { [catch { set fh [open {|some_prog} {w}] } ] } {
error "ERROR: $errmsg"
}
puts $fh "blah blah blah"
# most problems happen on close() instead of open():
if { [catch { close $fh } errmsg] } {
error "ERROR: $errmsg"
}
To read a sub-process's output, use "r":
# some problems (missing program) happen on open():
if { [catch { set fh [open {|some_prog} {r}] } ] } {
error "ERROR: $errmsg"
}
# read each line:
while {[gets $fh this_line] != -1} {
..
}
# most problems happen on close() instead of open():
if { [catch { close $fh } errmsg] } {
error "ERROR: $errmsg"
}
To both drive stdin and gets stdout, use "r+", and note that you will
probably need to call flush
before every gets
:
% set fh [open {|bc} {r+}]
file5
% puts $fh "2+2"
% flush $fh
% gets $fh
4
% puts $fh "5*4"
% flush $fh
% gets $fh
20
pid
pid
Returns the PID of the current process.
set mypid [pid]
Subprocessing with interp
Another way to run a subprocess is interp
. interp
creates a child
interpreter that can then run on a block of code in its own isolated
namespace. This can be used to run possibly unsafe code (say, something
given to you by a web page). They're sort of green threads -- they
don't run independently, and if one blocks they all block. Almost no
variables are shared (with the notable exception of $::env
), nor are any
file handles.
interp create
interp create [-safe] [<name>]
Creates a child interpreter. -safe
keeps the interpreter from executing
anything that might be harmful.
interp create "my_interp"
interp delete
interp delete <name>
Deletes the given child interpreter.
interp delete my_interp
interp eval
interp eval <name> [<arg>]+
Just like eval
, except the code is run in the child interpreter.
interp eval my_interp {
set foo 42
set bar 13
}
interp alias
interp alias <...>+
Creates a link between master/slave interpreters, so that they can access
each other's state.
(Instead of an example, I am hereby giving you a snarky comment that anything
this advanced should be done in an another language.)
introspection
Introspection is unique to interpreted languages. It allows the
programmers to ask questions about the program itself, such as "is there a
variable named 'foo'?" Tcl's command for introspection is info
, and it
has a bunch of sub-commands.
info exists
info exists <varname>
Returns whether or not the given variable name is accessible in the current
context. This is particularly useful for checking if a particular element
exists in an array.
set h(e1) "foo"
set h(e2) "bar"
if {[info exists h(e3)]} {
...
}
It is also useful for checking if a variable is defined:
unset -nocomplain errmsg ;# make sure 'errmsg' is undefined
catch { myfunction } errmsg
if {[info exists errmsg]} { ;# then there was an error
info script
info script
Returns the name of the script being run. (Same as $0
in perl, or
argv[0]
in C.)
if {[llength $::argv] == 0} {
puts "[info script] -foo -bar"
return -1
}
info level
info level [<number>]?
Returns the stack level at which the program is currently running, which
you can use to generate a stack trace. Positive values for number
will
instead return the procedure (and arguments) for that level. ("0" is the
global level.) Negative values do the same thing except they count up from
the current level instead of counting down from the global level.
puts "currently at level [info level]"
puts "called by [info level -1]"
info tclversion
info tclversion
Returns the version of the interpreter, such as "8.4".
set this_ver [info tclversion] ;# => this_ver is '8.4'
info patchlevel
info patchlevel
Returns the version of the interpreter, such as "8.4.6".
set ver [info patchlevel] ;# => ver is '8.4.6'
less useful "info" commands
info cmdcount
info cmdcount
Returns the number of commands that have been run so far by this program.
if {[info cmdcount > 100]} {
puts "whatever you're doing should probably be in another language!"
}
info commands
info commands <glob_pattern>
Returns a list of available Tcl commands that match the given pattern
.
set all_cmds [info commands *]
info functions
info functions <glob_pattern>
Returns the list of available math functions that match glob_pattern
. These
are the functions you can use in expr
commands.
set math_funcs [info functions *]
info globals
info globals <glob_pattern>
Returns the list of global variables that match glob_pattern
.
set globs [info globals *]
info locals
info locals <glob_pattern>
Returns the list of local variables that match glob_pattern
.
set locs [info locals *]
info procs
info procs <glob_pattern>
Returns the list of visible procedures that match glob_pattern
.
set procs [info procs *]
info vars
info vars <glob_pattern>
Returns the list of local and global variables that match glob_pattern
.
set vars [info vars *]
info args
info args <procname>
Returns the list of names of arguments to the given procedure.
proc foo { a b } { ... }
set arglist [info args foo] ;# => arglist is 'a b'
info default
info default <procname> <argname> <varname>
Returns whether or not arg argname
of procname
has a default value; if
it does, it also puts that default into varname
.
proc foo { a {b 0} } { ... }
info default foo a d1 ;# d1 is not set
info default foo b d2 ;# d2 is '0'
info body
info body <procname>
Returns the body of the given procedure.
puts "The code for the sqrt function is:\n[info body sqrt]"
subst
Another introspection-like ability is the subst
function, which tells Tcl
to do a round of variable and command interpolation. Tcl already does one
round as it's compiling the source code, but that's not enough to let you
do weird stuff like, say, construct a variable name on the fly.
% set a "foo"
foo
% set b a
a
% puts {No substitution: $a $$b}
No substitution: $a $$b
% puts "With Tcl's first level of substitution: $a $$b"
With Tcl's first level of substitution: foo $a
% puts [subst "With Tcl's plus a subst: $a $$b"]
With Tcl's plus a subst: foo foo
namespaces
One common problem that programmers run into is name collision. Tcl
lets you create hierarchical namespaces so that you can carve out your own
little sandbox. Tcl's namespaces look just like C++'s namespaces, in that
they're separated by double-colons, and the global namespace is an empty
string. However, Tcl also lets you selectively import or export symbols to
or from a namespace.
Namespaces are critical for working with packages.
In almost typical Tcl fashion, there's one Tcl command for namespace
functions, with several sub-commands.
namespace eval
namespace eval <ns_name> <code>
Defines the namespace in which code
lives. It looks very much like C++'s
namespaces, except that you can later define things in that namespace
outside of the "namespace eval" block.
namespace eval foo {
set a "foo" ;# => full var name is foo::a. Or ::foo::a.
}
namespace export
namespace export [-clear]? [<glob_pattern>]+
Adds any symbols matching pattern
to the list of this namespace's
exported symbols. This is incremental, so you can call it many times to
add things; if you don't want it to be incremental, use the -clear
switch to
clear out all the existing symbols first.
namespace eval foo {
namespace export *
}
namespace import
namespace import [<glob_pattern>]+
Imports any commands matching glob_pattern
into the current
namespace. Namespaces kind of act like procedures, in that once you're in
one you can't see anything in any parent scopes. You can use
namespace import
to let
a seemingly local variable actually refer to something in another package,
which saves you from having to type a lot of scope-resolution over and over.
namespace eval {
namespace import foo
put "foo::a = $a" ;# "$a" actually picks up "$foo::a"
}
namespace current
namespace current
Returns the name of the current namespace, fully qualified all the way to
the global namespace.
puts "vars in [namespace current] are: [info vars]"
namespace delete
namespace delete <ns_name>
Deletes the namespace, including everything in it. I have absolutely no idea
what value there is in being able to delete namespaces on the fly, but if you
come up with one, you'll be a happy Tcl user.
namespace delete foo
namespace ensemble
namespace ensemble
Lets you access a namespace exactly like one of Tcl's typical
major-commands-with-subcommands. Unfortunately the docs are thin on how to
use this, and this requires Tcl 8.5 so I don't have the man page for it.
variable
variable [<name> <value>]+
Declares variables that are local to a namespace. The intent of this function
is to encapsulate variables so that they don't pollute the global namespace.
They sort of act like global variables (in that you can access them from
multiple procedures, regardless of calling context), but won't collide with
either global variables or variables in other namespaces.
namespace eval myspace {
variable foo
}
proc ::myspace::myfunc {
variable foo ;# now $foo refers to this namespace's "foo"
}
Example
namespace eval foo {
proc bar {} {
}
}
namespace eval fee {
proc bar {} {
}
}
bar ;# not found
foo::bar ;# found
fee::bar ;# found
Example for imports/exports:
namespace eval foo {
namespace export bar
proc bar {} {} ;# true path is "::foo::bar"
}
namespace eval asdf {
# with or without any export or import, it can be accessed like this:
::foo::bar
# with only export, we can still only use ::foo::bar
# with only import, we can still only use ::foo::bar
# with both import and export, we can do this:
namespace import ::foo::bar
bar
}
# with or without any export, it can be accessed like this:
::foo::bar
packaging with "source"
The first approximation to software packaging comes from the source
command, which is exactly like csh's source
and somewhat similar to C's
#include
. The source
command tells the Tcl interpreter to go execute
the commands in the given file. If there's a return
in the sourced file,
the interpreter returns back to the original program.
source
can be used to approximate several common mechanisms:
- data loading. Instead of parsing a custom file format with regexs, you
could just set up the data file as a bunch of Tcl
set
commands.
- code modularization. Instead of copying a useful procedure into every
Tcl script you write, you could put the procedure in a common Tcl file and
source
it from each your scripts.
- code organization. Instead of having a single huge script,
you could break it into pieces to make it easier to manage.
source
returns the value of the last statement executed in the script,
and if there's an error in the script, source will return that. The best
way to tell if a source was successful is to run it through catch:
if {[catch {source foo.tcl}]} { ... }
You can get the return code from source while catching:
if {[catch { set res [source foo.tcl]}]} { ... }
Note that if the source
fails, then the 'set res
' will not be executed,
so you have to check the result of the catch
before using res
. Here's
another way to do that exact same code, but unrolled a bit for illustration:
set catch_res [catch {set source_res [source foo.tcl]}]
if {$catch_res} {
..catch caught something, so source had an error..
} else {
..catch caught nothing, so source was okay, and you can use $source_res...
}
packaging with "package"
Packages are the next step up from using source
as a way to modularize
code. They are much more similar to C's #include
, and perl's 'use
'.
You can package things up without using namespaces, and you can have
namespaces without using packages, but from a software engineering
perspective the best practice is to use them together.
package provide
package provide <pkg_name> [<pkg_version>]?
Declares that the package named pkg_name
now exists, with the given
(optional) version. (Multiple versions of a given package are allowed to
be loaded at the same time!)
package provide my_module
package require
package require [-exact]? <pkg_name> [<pkg_version>]?"
Loads the given package. If you specify a pkg_version
, then it will
load any member of its major family that's at or after your specified
version; with -exact, only the version you requested will be loaded.
package require tclxml
pkg_mkIndex
pkg_mkIndex <dir> [<pattern>]+
Creates a pkgIndex.tcl
for you. When you're done creating your package,
you'll want to run this (yes, from the interpreter) to generate a pkgIndex.tcl
file. Then you'll want to install pkgIndex.tcl
and your .tcl file into
a directory with the package name, as as sub-directory of something in
$::tcl_pkgPath
or
$::auto_path
, so that "package require
" will find it. (Or, consumers
could lappend auto_path $somedir
to pick it up from wherever you want
to put it.)
Note: you must have a namespace in order for pkg_mkIndex to generate a
nonempty file.
Note: in practice, a declared version number is required, because the
package ifneeded
call in pkgIndex.tcl
needs it.
Of course, there's only one line in pkgIndex.tcl
anyway, so if you really
wanted you could just assume manual ownership of it:
package ifneeded Foo 0 [list source [file join $dir Foo.tcl]]
where you replace "Foo"s with your package name, and "0" with your version.
Do not change "$dir" -- leave it as a variable; it gets magically filled
in by the package code.
Putting packages and namespaces together
Packages allow you to bundle code and data together into some logical unit, and
namespaces reduce the chances that your stuff will cause a name collision. To
put the two together, it is highly recommended that you choose the same name for
the package and the namespace.
This is as close as Tcl gets to object-oriented programming. It works almost
the same way that C implements objects: by passing a token to procedures to
indicate which object to use.
package provide mystack 1.0
# declare a namespace:
namespace eval mystack {
# declare our procs:
namespace export new push pop
# declare our variables, though they're really private:
variable stack
variable stack_counter 0
}
# define procs:
proc ::mystack::new {} {
variable stack
variable stack_counter
set stack_name "stack[incr stack_counter]"
set stack($stack_name) [list]
return $stack_name
}
proc ::mystack::push { stack_name val } {
variable stack
lappend stack($stack_name) $val
}
proc ::mystack::pop { stack_name } {
variable stack
set res [lindex $stack($stack_name) end]
set stack($stack_name) [lrange $stack($stack_name) 0 end-1]
return $res
}
The consuming code for this mystack object looks like this:
package require mystack
set mystack [mystack::new]
mystack::push $mystack foo
mystack::push $mystack bar
..
set val [mystack::pop $mystack]
eval
eval
allows you to create and run Tcl code on the fly. Anyone with a
deep history of compiled languages will be baffled by this, but it's quite
common in interpreted languages. It's also fairly dangerous, so you're
warned.
eval
eval <command> [<arg>]*
Runs the given Tcl command
with the given arg
s, as if you had run them
directly. One reason to use eval
is so that you can trap exceptions.
Another good reason for using eval
is to expand out the parameters in
a Tcl command string that you've assembled on the fly:
proc plus1 { a } {
puts [expr $a + 1]
}
proc minus1 { a } {
puts [expr $a - 1]
}
set cmd {plus1 4}
set cmd {minus1 4}
$cmd ;# this does not work; there is no 'minus1 4' function
eval $cmd ;# this works; Tcl now sees the '4' as a parameter to 'minus1'
The "best practice" for creating lines to eval is allegedly to use lists and
lappend
. The above example should then be:
...
set cmd [list minus1 4]
...
or
...
set cmd minus1
lappend minus1 4
...
system
cd
cd <dirname>
Changes this process's working directory to the given dirname
.
cd workdir
pwd
pwd
Returns the current working directory.
set orig_dir [pwd]
...
cd $orig_dir
argv/argc
Command-line options are stored in global variables:
- $::argc: the number of arguments passed to the script
- $::argv0: the name of the script itself
- $::argv: a list of the command-line arguments
(if you want the name of the tcl interpreter, instead of the name of the
Tcl script being executed, use 'info nameofexecutable
'. That gives you
the Tcl process's $0
.)
ENV settings
Environment variables are read and written through the global array $::env
.
puts "Hello, $env(USER)!"
clock seconds
clock seconds
Returns the number of seconds since 1/1/70.
set start_time [clock seconds]
...
set end_time [clock seconds]
puts "... took [expr $end_time - $start_time] seconds"
clock format
clock format <value>
Formats the given time, to select which fields are shown, and how.
set curr_time [clock seconds]
set file_time [file mtime "foo.txt"]
if { file_time < curr_time - 300 } {
puts "foo.txt modified more than 5 minutes ago"
puts "current time: [clock format $curr_time]"
puts "file modified: [clock format $file_time]"
}
clock scan
clock scan <string>
Converts human-readable time to the number of seconds since 1/1/70.
set curr_time [clock seconds]
set pretty_string [clock format $curr_time]
if { [clock scan $pretty_string] != $curr_time } {
puts "ERROR! 'clock scan' didn't get the original value!"
}
exceptions (and other errors)
Historically there have been three common ways for procedures to indicate
errors:
- their return value could be an integer code, with some convention for
determining which numbers mean "error"
- procedures could set a global variable to indicate there was a problem
(C's
errno
)
- procedures could throw an exception, which unrolls the stack until it
is caught
Tcl uses all three. Whee!
The return-code method is straightforward, so there's nothing new to cover
there.
The global variables for Tcl errors are $::errorCode
(which is a number)
and $::errorInfo
(which is a string describing the error). Only some
functions set these, which means it's best not to rely on them, but it's
good to know they exist.
Tcl's documentation bends over backwards to avoid using the word
"exception", though they do slip up a couple times. In Tcl parlance,
throwing an exception is called "generating an error".
error
error <string> [<errcode> [<info>]?]?
Throws an exception with the given text. If you provide errcode
and/or
info
, the global variables $::errorCode
and $::errorInfo
are set.
if { $val < 0 } {
error "negative value! bad! ugga!"
}
catch
catch <code> [<varname>]?
Catches any exceptions thrown in code
. If you give a varname
, and
code
executes without an error, the last value of code
is put into
it, and catch
returns 0. If code
executes with an error, catch
returns 1.
if {[catch { set fh [open "foo.txt"] } errmsg ] } {
puts "ERROR> could not open 'foo.txt': $errmsg"
}
Note that if there is no exception, then errmsg
above would not be
defined, so trying to use it would cause a runtime exception. On the
other hand, if the open
did throw an exception, then fh
would not
be defined, so trying to use it would cause a runtime exception.
Have fun! :)
return
return [-errorcode <num>] [-errorinfo <string>] [<value>]?
The return function actually lets you generate an exception at the same
time as returning a value.
if { $val < 0 } {
return -errorcode -1 -errorinfo "negative value! bad! ugga!"
}
Debugging
error messages
Tcl's error messages are horrible. They are essentially stack traces, but with
relative line numbers. Here's the example from tcl.tk, showing the error that
occurs when you call a function that doesn't exist:
input file
1 proc a {} {
2 b
3 }
4 proc b {} {
5 c
6 }
7 proc c {} {
8 d
9 }
10 proc d {} {
11 some_command
12 }
13
14 a
execution output, with snarky annotations:
invalid command name "some_command" <= the actual error is first, at least
while executing
"some_command"
(procedure "d" line 2) <= line 2 of 'd' is actually line 11
invoked from within
"d"
(procedure "c" line 2) <= line 2 of 'c' is actually line 8
invoked from within
"c"
(procedure "b" line 2) <= line 2 of 'b' is actually line 5
invoked from within
"b"
(procedure "a" line 2) <= line 2 of 'a' is actually line 2..by pure coincidence.
invoked from within
"a"
(file "errors.tcl" line 14) <= note that line 14 isn't where the problem was
The way to read the error message is:
- read the first line, to see what the actual error is
- skip to the bottom to find the absolute line number to start on
- work your way up the "invoked from within" blocks, manually counting line
numbers in functions to find the actual line with the error
Of all the crappy parts of Tcl, compiler error reporting is, hands-down, Tcl's weakest link.
trace
There's a trace
command that will let you watch variables and
commands. It's not quite a full debugger (it doesn't let you step through
execution or anything), but if you're interested you should check out the
man page for it. And possibly the official
tutorial page as well.
profiling
Tcl has a built in function for timing how long a block of code takes to run:
time
time <code> [<count>]?
Runs code
, count
times, and returns the average amount of (wall) time
it took on each run.
set avg_time [time { exec "/bin/ls" } 100]
puts "ls takes about $avg_time seconds per run"
sockets
It seems hard to believe, but Tcl -- the scripting language world's answer
to assembly language -- has direct support for networking sockets. Which
I'm definitely not going into detail about - go see
the tutorial.
But if you're grepping this file for function names, here are the ones
of interest:
- socket -server <command> [<arg>]* <port>:
creates a network server
- socket <port>:
creates a network client
- fileevent <channel> [readable<i>writeable] [<code>|]?:
defines a callback to be invoked whenever the
channel
has input or
output to process.
- vwait <varname>:
suspends the current process until the variable named
varname
is set.
- fblocked <channel>:
returns whether or not the given
channel
has more input
- fconfigure <channel> [...]:
lets you set up all sorts of configuration info for the given
channel
, such as blocking vs. nonblocking, buffersize, and
binary vs. ascii
Chris verBurg
2014-07-22