Troubleshooters.Com®, Code Corner and Tcl Town Present:
Tcl Secret Sauce
Copyright © 2019 by Steve Litt
See the Troubleshooters.Com Bookstore.
CONTENTS
Like every other technology, Tcl has gotchas and unexpected situations that are inconsistently documented across the Internet. These gotchas can keep you from getting to first base in learning Tcl. This document enables you to get past these gotchas, in code you can easily understand, so you can get on with the business of learning Tcl.
This document was written in early 2020 and has been tech-edited for
Here's the simplest Hello World Tcl program:
#!/usr/bin/tclsh puts "Hello world!"
Name the preceding file hello.tcl, and perform the following command on it:
chmod a+x hello.tcl
When you run the preceding code, you should get the following output:
[slitt@mydesk tut]$ ./hello.tcl
Hello world!
[slitt@mydesk tut]$
If you get a different result, troubleshoot. Did you mark the file as executable? Is tclsh really in /usr/bin, or is it somewhere else on your computer? Did you put doublequotes around the Hello World! string? Take note of all error messages, and carefully get this script so it does the right thing. Don't continue with this document until you get this right, because you can't do Tcl without being able to get it running in your environment.
Create the following file, call it hello_e.tcl:
#!/usr/bin/tclsh puts "Hello world!" puts -nonewline {It's a nice "day" }; puts {for programming} puts -nonewline "in Tcl" puts {.}
The preceding code has the following output:
[slitt@mydesk tut]$ ./hello.tcl
Hello world!
It's a nice "day" for programming
in Tcl.
[slitt@mydesk tut]$
The code in this section is a tale of five puts calls:
Study this until you're comfortable with it and have committed these two string forms and the covered puts calls to memory.
Tcl has an interactive development environment available by running tclsh with no command line arguments. Unfortunately, as it ships from the factory, tclsh doesn't recall earlier commands with handy up and down arrows.
GNU puts out a program called rlwrap. Most distros package it, so install it. From then on, you can have Bash like command line history and editing within tclsh by starting it in the following manner:
rlwrap tclsh
NOTE:
tclsh ships with bang style history using the exclamation point, but that's not half as quick and easy as what rlwrap arrow key history and command line editing give you.
tclsh ships with bang style history using the exclamation point, but that's not half as quick and easy as what rlwrap arrow key history and command line editing give you.
From Fortran on down, most languages use a single equal sign (=) to perform assignment. Well, except Pascal that used ":=". Tcl uses none of that: It uses a set statement as follows:
set myvarname myvalue
If you forget this, you'll waste a lot of time. Never use a single equal sign.
As mentioned before:
set myvarname myvalue
The preceding code assigned myvalue to variable name myvarname. When you use a variable name, as opposed to assigning to it, you need to tack a dollar sign on the front. Thus:
set myvarname myvalue puts $myvarname
When you forget to use the dollar sign, and if you're used to other languages you will, you'll either get a literal (like the literal myvarname, or worse, a cryptic error message.
When things don't go right, always check new code to make sure you haven't left out a dollar sign when you use a variable.
To find your tclsh version, perform the following within tclsh:
info patchlevel
Copy and paste the following code exactly into a file, permission the file executable by all:
#!/usr/bin/tclsh foreach evar [array names ::env] { puts "$evar ===> $::env($evar)" }
Careful: the word "names" is a reserved word: One of many commands you can perform on an array. The double colon before every instance of env puts env, the environment array, in the global namespace, which is usually what you want to do with the environmental namespace. When run, the preceding program shows you all the environment variables.
Now expand the preceding so it shows off the directories in the special $auto_path variable, and also has headers and tailers to let you know what's what:
#!/usr/bin/tclsh puts "====== BEGIN ENV VAR ARRAY ======" foreach evar [array names ::env] { puts "$evar ===> $::env($evar)" } puts "====== END ENV VAR ARRAY ======" puts "====== BEGIN \$auto_path LIST ======" foreach dir $auto_path { puts $dir } puts "====== END \$auto_path LIST ======"
NOTE:
You've probably noticed that the syntax of the two loops in the precednig code are different from each other. This is because $env is a Tcl Array, but $auto_path is a Tcl List. The difference is discussed in the next section.
CONFUSION ALERT:
Few Tcl behaviors are as confusing as its use of square brackets, curly braces and parentheses.
It's easy to mix up the functions of square brackets, curly braces, and parentheses in Tcl. This is especially true because Tcl's use of these punctuations is very different from most other computer languages. This section gives you needed rules of the road.
Square brackets have exactly one use in Tcl: To delineate a command and expose its return value. If you have a three word command, and you want to use it as an argument to a different command, then you must put braces around the first command. The following is an example:
puts [lindex $mylist 3]
The preceding is a puts command that uses the return value of the multi-word lindexcommand. The puts command expects exactly one argument, so to facilitate that, square brackets surround the lindex command and its arguments.
The preceding is the only function of square brackets in Tcl.
Tcl uses parentheses for two distinct purposes:
The following code illustrates the former usage: Specification of a specific key in an array:
set favecolors(Mae) Gray puts $favecolors(Mae)
CONFUSION ALERT:
In the set command, you might think favecolors should start with a dollar sign because the array is a variable and you're just adding an element. That makes perfect sense, but it's not how Tcl works: If you add an element to an array, you don't use the dollar sign.
The following code illustrates the latter usage: Holding arguments for expr commands:
set mycosine [expr cos(3.14/4)]
Tcl uses curly braces to fill the following needs:
Tcl has a data type called List, and another called Array. For the person who fluent in other computer languages, or who has taken courses in Computer Science, Tcl's use of the term Array could be very misleading, and could stop learning dead in its tracks. Hence this section.
There are two common types of collections in almost every evolved language:
That's right: What's called an array in Tcl is really an unordered collection of key-<value pairs, just like Perl hashes, Python dicts, Ruby hashes and Lua tables. And unlike Lua tables, Tcl array keys are never integers, and instead always strings.
Unless you consciously know and remember this, it could act as slippery mud causing you to slide down the learning curve. Arrays in Tcl are quite different from traditional arrays.
The following code introduces the creation and listing of a Tcl list and selection of list item 3:
#!/usr/bin/tclsh set top5 { list {Wilkes} {Jackson} {Van Dorn} {Khan} {d'Angeles} } foreach champ $top5 { puts $champ incr ss } puts "" puts [lindex $top5 3]
The preceding code prints the word "list", then prints out the list items in order, and then prints out list item 3, which is "Van Dorn". The word "list" prints because it's the 0th item in the list. So the 0th item is the type of the variable. That makes the actual list items 1 based: The earliest item is #1, not #0 (like in C).
The puts [lindex $top5 3] line retrieves and prints item #3.
The "list" printout can be eliminated by not printing item # 0, as shown in the following code:
#!/usr/bin/tclsh set top5 { list {Wilkes} {Jackson} {Van Dorn} {Khan} {d'Angeles} } set ss 0 foreach champ $top5 { if {$ss > 0} { puts "$ss $champ" } incr ss } puts "" puts [lindex $top5 3]
Tcl is built around a philosophy that shouldn't be studied until you're more experienced with Tcl. But there are a few tidbits of that philosophy that you can use to push away confusion right now...
Here's how you read a file:
#!/usr/bin/tclsh set f [open "/etc/fstab" "r"] set data [read $f] puts $data
In the preceding, you set f to the return of the open statement, which is in brackets so it's taken as one argument to the set command. The open command takes two arguments: The filename, and the read/write mode, which is "r" for read.
Here's how you write a file:
#!/usr/bin/tclsh # NOTE: 3RD arg to open, the permission, is crazy # Experiments failed to indicate how it works. set f [open "./steve.litt" "w"] puts $f "Steve Litt's file.\n" close $fSame thing except since we're writing the file, "w" is used for the read/write mode. There are actually several other modes: They're the same as in the C language and several others, so you can look them up in Tcl docs or elsewhere. There's actually an optional third argument to open, representing the permissions of a newly written file, but I've never been able to get that third argument to work properly.
You'll often want to run another program from Tcl. The simplest case is running a GUI program, as follows:
#!/usr/bin/tclsh open |gimp puts "Continuing on ..."
The preceding Tcl program runs gimp and then continues on its way, independent of what Gimp's doing. If you want the Tcl program to stop (block) until gimp finishes, you need to assign the return of the open to a file variable, as follows:
#!/usr/bin/tclsh set temp [open |gimp] catch {close $temp} err puts "After close." if {$err != ""} { puts "err is $err." } puts "Continuing on..."
The preceding stops until Gimp terminates, then closes the file assigned by the open. Notice the following line:
catch {close $temp} err
The preceding runs the close of filevar $temp, but catches any error to prevent the entire script from terminating. This is handy when running other programs, because you don't (necessarily) want the non-zero return from the program that got run to abort your entire Tcl program. If the program returns non-zero, $err contains a text error message from the program that was run.
You'll often want to run a simple command from Tcl. If your Tcl program doesn't need to feed the spawned command's stdin nor capture its stdout, it's pretty easy:
#!/usr/bin/tclsh set temp [open "|touch junk.jnk"] catch {close $temp} err puts "After close." if {$err != ""} { puts "err is $err." } puts "Continuing on..."
It's pretty easy to run a command with pipes and redirects. Consider the following program, which puts all output of the mount program that begins with /dev/ into file junk.jnk:
#!/usr/bin/tclsh set pgm [open {|mount | grep ^/dev/ > junk.jnk} "w" ] catch {close $pgm} err if {$err != ""} { puts $err }
If you're going to shell out at all, sometimes it's best to do everything there, including piping and redirection.
NOTE:
If you don't know what piping and redirection are, see https://ryanstutorials.net/linuxtutorial/piping.php
Sometimes you need to capture the stdout from the program you run. Here's how you do it:
#!/usr/bin/tclsh set pgm [open {|/bin/ls -ltr} "r" ] set data [read $pgm] catch {close $pgm} err if {$err != ""} { puts $err } else { puts $data }
A filter program is a program that takes its stdin, modifies it, and puts the modified content out its stdout. For instance, the command cat -n takes in a series of lines in its stdin, and sends out those same lines in the same order, but numbered, out its stdout.
Sometimes you want your Tcl program to send a series of lines to a filter program and get back a processed series of lines. This is a little tricking in Tcl, but only a little. Consider the following program:
#!/usr/bin/tclsh set pgm [open {|cat -n} "r+" ] puts $pgm "one" puts $pgm "two" puts $pgm "three" # Next two lines required # So program doesn't stall flush $pgm chan close $pgm write set data [read $pgm] catch {close $pgm} puts $data
Creating, enabling and using your own Tcl package is more complicated than it is in most other interpreters. I vigorously advise you to do it with a "Hello World" setup before doing it with your real package, in order to drastically limit the number of moving parts obfuscating the situation. This section walks you through a minimum-moving-parts hello world in its own directory.
package provide mypkg 1.0 package require Tcl namespace eval mypkg { proc id {} { puts "This is mypkg" } }
#!/usr/bin/tclsh puts $auto_path set auto_path [linsert $auto_path 0 /d/at/tcl/test/mwe] puts $auto_path package require mypkg ::mypkg::id
You made a tiny package file, mypkg, and imported it via a require command into mwe.tcl, then proved the import by running mwe.tcl and seeing the "This is mypkg" string at the bottom.
The mypkg.tcl package file creates a namespace named "mypkg", that includes the procedure id, which prints "This is mypkg". Then, within tclsh, you run pkg_mkIndex -direct in order to create necessary file pkgIndex.tcl. Application file mwe.tcl prepends the full pathname of the current directory to $auto_path so mwe.tcl can reach out to the prepended directory (in this case the current directory, consult file pkgIndex in that directory, and import the package that implements mypkg.
Once you've gone that far, you can do things like putting the package file in a different directory and rerunning pkg_mkIndex from ttclsh in that directory adding things to it, and filling out the application that required it.
It's well known that everything is a string in Tcl, but just try to make a string perform as a command. It's pretty hard. Consider a situation where I want to run, as a command, a string called cmd, which contains a puts command. All sorts of attempts fail, a few succeed, but here's one that handles most of the cases:
#!/usr/bin/tclsh set cmd {puts "Hello World!"} puts $cmd eval $cmd
The preceding first prints the string that will be used as the command, and then the result of running that command via eval. This method also handles the more complex case in which the ""World" is replaced by a variable called city:
#!/usr/bin/tclsh set city "Los Angeles" set cmd {puts "Hello $city!"} puts $cmd eval $cmd
The preceding code outputs the following:
[slitt@mydesk tut]$ ./test.tcl puts "Hello $city!" Hello Los Angeles! [slitt@mydesk tut]$
Here's the logic of the preceding code. The eval command evaluates a string, as long as the string is set up as a proper list. First city is set to "Los Angeles": Nothing unusual there.
Next we set cmd to the do-not-substitute string {puts "Hello $city!"}. The purpose of the doublequotes is to keep together the argument string for puts while also making sure that puts has exactly one argument. The purpose of the curly braces is to make sure that no substitutions take place, so the value of cmd is literally:
puts "Hello $city!"
Then the eval runs that command, and outputs "Hello Los Angeles".
There's another way to do it. The preceding method left the variable replacement of city until the eval command. The following does the substitution when the cmd string is set:
#!/usr/bin/tclsh set city "Los Angeles" set cmd "puts \"Hello $city!\"" puts $cmd eval $cmd
The preceding code produces the following output:
[slitt@mydesk tut]$ ./test.tcl puts "Hello Los Angeles!" Hello Los Angeles! [slitt@mydesk tut]$
The output of the eval command is the same, but the value of cmd is literally:
puts "Hello Los Angeles!"
So this time, the actual city name is in cmd rather than a variable representing the city. There are probably several ways to use a string as a command, but there are probably ten times as many ways such an effort can fail.
CONFUSION RISK:
It's very difficult to correctly use a string as a command, because there are so many right-looking but wrong ways to do it. Remember to look back in this document when you need to use a string as a command.
If you've read and understood all the sections of this document, and hopefully performed the exercises therein, you're in an excellent position to learn Tcl, a great language with many unexpected situations that can scuttle learning if not approached right. Now, equipped with this knowledge, go back to the Tcl Town homepage to continue your Tcl education, including both Troubleshooters.Com hosted material and some carefully curated offsite links.
It will be worth it. Tcl is a good language with lots of libraries that develops lightning fast.
[ Training | Troubleshooters.Com | Email Steve Litt ]