The Tcl scripting language has been around a long time, and it tends to keep a pretty low profile. In fact, it's so long and so low, if you're not of a certain age and not a language junkie, you may never have even heard of it.
Despite its lack of headlines, Tcl still has a "vibrant community" (according to its website), and it continues to evolve, albeit slowly: versions 8.6.8 and 8.6.9 were released 11 months apart.
Tcl stands for Tool Command Language, and it originally was designed to be used as a language for embedding inside other applications. It still can be used for that, but it also has found success as a standalone scripting language.
If you've ever used or heard of git (who hasn't at least heard of it), the default GUI that comes with git is written in Tcl, using Tcl's GUI toolkit Tk.
If you do any embedded programming with ARM CPUs, the popular tool OpenOCD uses Tcl as its embedded programming language.
Tcl is not a scripting language like Bash in the sense that it does not "script" Linux commands.
In other words, when writing Tcl, you aren't generally executing
grep
and sed
or any other commands that you
normally would
type at the command line, you're executing the commands that are
built in to Tcl.
In this sense, Tcl is more akin to Python than to Bash.
In this article, I want to do five things:
You can assign values to variables:
set a_num 99
set a_string "some string"
set a_list { 99 100 101 102 }
set an_array(0) 12
set an_array(1) 13
As you might expect for a scripting language, comments are lines that start with a hash sign (#). But contrary to what you might expect, hash signs on the same line as code are not seen as comments:
# This is a comment.
set a 12 # WRONG: this is not a comment
Within a "value", three types of substitution are done:
$name
is replaced
with the value of the variable name
).
[cname...]
is replaced
with the return value of the command cname
).
This is akin to Bash's backticks or its $(...)
syntax.
\<char>
is replaced
with the character <char>
).
For example:
# Set a to 99 and b to 100
set a 99
set b [expr $a + 1]
set c \n
In set b
above, the command [expr ...]
evaluates
its arguments as an expression and returns the result
(all the "normal" operators are available).
All of these substitutions also work inside double-quoted strings:
set a 99
set b " a is $a\n setting b to [expr $a + 1]"
Somewhat unexpectedly though, no substitutions are done in lists:
# WRONG: the first element of a_list will be "$a" not 99.
set a 99
set a_list { $a 100 101 102 }
If you want to do the above, you need to use the list
command:
# RIGHT: the first element of a_list will 99.
set a 99
set a_list [list $a 100 101 102]
The reason for this strangeness will become clear in a bit.
Tcl also has control statements:
# if statements:
if { $a > 20 } {
puts "a is greater than 20"
} elseif { $a > 10 } {
puts "a is greater than 10"
} else {
puts "a is less than or equal to 10"
}
# loop statements:
foreach var $list {
puts "$var"
}
foreach var {1 2 3 4} {
puts "$var"
}
Tcl has functions as well:
proc myfunc {arg1 arg2} {
puts "arg1 is $arg1"
puts "arg2 is $arg2"
}
# Call function:
myfunc 12 13
proc mysum {a b} {
return [expr $a + $b]
}
# Capture return value of function:
set sum [mysum 12 13]
Note that in the examples above, the curly brace that starts a "block" of code must be on the same line as the previous part of the statement:
########
# WRONG:
########
if { $a > 20 }
{
}
elseif { $a > 10 }
{
}
else
{
}
foreach var $list
{
}
proc myfunc {arg1 arg2}
{
}
To overcome this, you can can escape the newline before the start of the block:
###########################
# OK (but not very Tclish):
###########################
if { $a > 20 } \
{
}
elseif { $a > 10 } \
{
}
else \
{
}
foreach var $list \
{
}
proc myfunc {arg1 arg2} \
{
}
Again, the reason for this strangeness will be revealed shortly.
So except for using curly braces {...}
around
control statement conditions and formal function parameters,
there's nothing too syntactically strange in Tcl.
Speaking of syntax, let's take a step back and look at Tcl's syntax in general.
It's only a slight exaggeration to say that Tcl's syntax can be defined in a single line:
WORD...
I know what you're thinking: "wait, you just showed me if statements, loops and functions, so how can one word and some ellipses define Tcl's syntax?"
Obviously, syntax alone doesn't define a language; you also need to understand the semantics or meaning of the syntax.
First, it's important to know what a WORD
is in Tcl:
In all but the last item above, backslash substitution,
variable substitution and command substitution is done.
For the first item above, somewhat unexpectedly perhaps,
this means unquoted words can include $var
and [command]
substitutions (as well as backslash
substitutions);
see the examples below.
Curly braces are a bit like Python's triple quotes
(""" ... """
).
Some sample words:
avar
some_name
# if b has the value 12, this will be the word "a12"
a$b
# if a is 1 and b is 2, this will be the word "c3"
c[expr $a + $b]
"double quoted string"
"$vars [commands] and backslash\n expansion is done"
{ this is one word }
{ this
is
one
word
too
}
I mentioned earlier that a WORD
can be "most any
sequence of characters without spaces", so you also
can write code like the following, although you won't make
many friends doing stuff like this:
set a'b'c 12
set a\"bcd\"e 99
The second thing to understand about the semantics of Tcl is that in Tcl everything is a command.
The first word on a line is the command, and the words that follow are the arguments to the command. So, the truth is that Tcl doesn't have an "if statement"; it has an "if command" that looks like this:
if COND_WORD THEN_CODE_WORD optional-elseif-else-words
And since WORD
s can be curly-brace blocks that span lines,
if
commands look like they should.
So the following if
command:
if { ... } {
...
} else {
...
}
Consists of five WORD
s:
if
.
if
condition between braces { ... }
.
{\n ... \n}
.
else
.
else
code block {\n ... \n}
.
Okay, so there are no if
statements, just if
commands.
At this point, you're probably thinking that this seems like a distinction
without a difference, and most of the time it is, but not always.
For instance, let's say I'm working with a bunch of dates, and I have a bunch of code that checks to see if my date value is on a Monday:
# [clock scan ...] - converts a string to a time
# [clock format ...] - similar to Linux's "date +FORMAT"
if { [clock format [clock scan $date] -format %A] ==
↪"Monday" } {
puts "It's Monday"
# ,..
}
And further, let's say I get a bit tired of typing all that, so I decide to do this:
proc if_monday {date block} {
set day [uplevel clock format \[ clock scan $date \]
↪-format "\{%A\}" ]
if { $day == "Monday" } {
uplevel $block
}
}
Now I can just do the following:
if_monday { $date } {
puts "It's Monday"
# ,..
}
In other words, I just added a new command named if_monday
that looks just like
an if
"statement"
(for the sake of simplicity, I'm going to ignore the else part here).
The secret sauce here is the uplevel
command.
uplevel
evaluates its arguments (the if expression or the if code block)
in the context of the caller of the if_monday
command.
To see what this means, consider this implementation and its usage:
# WRONG:
proc if_monday {date block} {
set day [uplevel clock format \[ clock scan $date \]
↪-format "\{%A\}" ]
if { $day == "Monday" } {
# WRONG
eval $block
}
}
set var 99
if_monday { $date } {
puts "It's monday"
set var 100
}
# var will have the value 99 not 100
puts "$var"
Since the if_monday
command evaluated the code block
using eval
rather than uplevel
, the set var
100
that's
inside the code block sets a variable named var
in the
if_monday
procedure and not the var
that's right before
the call to if_monday
.
On the other hand, if you use uplevel
instead of
eval
, the
correct version of the variable gets set—the one that's
in the "context of the caller".
Not to beat a dead horse, but again, commands are just a sequence of words. Given these stub functions:
proc start {} { puts start }
proc stop {} { puts stop }
I also can write my if
statements like this:
if 1 start else stop
if [expr 1 == 0] start else stop
The if
condition and the code blocks don't have to be inside
curly braces; they just need to be something that Tcl considers
to be a WORD
.
Note, however, that there's a subtle, probably undesirable side effect of doing stuff like this. Consider the code:
proc test {v} { puts $v; return 1 }
if 1 start elseif [test 44] { puts "elseif" } else stop
If you run this, the output will be:
44
start
Because the elseif
condition is not inside
curly braces, it is evaluated "before" the if
command (so that the
result can be passed as one of the arguments to the if
command).
So, although you can skip the curly braces, don't.
Earlier I mentioned there were a number of things that seemed strange and that I'd get back to them. Here they are:
1) Putting the code block or key word to a control statement on a newline:
# WRONG:
if { ... }
{
}
else
{
}
This doesn't work because the "then" code block is on a newline
and, therefore, is not seen as an argument to the if
command.
Similarly, the else
and its code block also are on separate lines
and are not seen as part of the if
command.
2) Using variable substitution when creating a list with curly braces:
set a 99
set a_list { $a 100 101 102 }
This doesn't work because no substitutions of any kind are done inside
a curly-brace delimited WORD
.
You also now probably can understand why Tcl has a set
command and
not an assignment statement, since each line needs to start with a
command name, so a = 12
wouldn't fly, as "a" is not a command name.
Because Tcl consists of "commands", another somewhat unexpected thing
that you see are "control" statements with options—for example,
the switch
statement looks like this:
set var def
switch $var {
abc { puts "won't match this one" }
def { puts "should match this one" }
}
The switch
statement also accepts options—for example, the
-glob
option:
set var def
switch -glob $var {
abc { puts "won't match this one" }
d*f { puts "should match this one" }
}
With the -glob
option, the matching is done using glob-style matching.
Note: Tcl predates the era of double-dash options, so all the standard
Tcl commands use single-dash options.
Before wrapping up this section, I want to note one thing about Tcl's variable expansion that should come as a relief to any Bash programmers who have ever been caught in "quote hell": once Tcl expands a variable, it doesn't do any further interpretation of the resulting value.
For example, consider the following Bash code:
function one_arg_func()
{
echo $1
}
When executed, you get:
$ one_arg_func 1
1
$ one_arg_func "1 2"
1 2
That seems fine, but now try this:
$ a="1 2"
$ one_arg_func $a
1
What happened? Bash happened. It expanded $a
and then reinterpreted
the value as being two separate words and passed two arguments to
the function rather than one.
Tcl doesn't do that:
proc one_arg_func {arg} {
puts $arg
}
set a "1 2"
one_arg_func $a
Tcl expands the variable a
once, and that's it, whatever value it has
is the value of the argument to the command, regardless
of embedded spaces or quotes or anything else that it has in its value.
In Tcl and the Tk Toolkit, John Ousterhout, the originator of Tcl, states:
Tcl's substitutions are simpler and more regular than you may be used to if you've programmed with UNIX shells (particularly csh). When new users run into problems with Tcl substitutions, it is often because they have assumed a more complex model than actually exists.
The previous examples have contained some Tcl commands; in this section, I want to cover just a few more commands to give you a bit of a feel for what you can do with Tcl, and for how you do it.
Read and write files:
# Open and read input file then close the file.
set fd [open "infile.txt" "r"]
set fdata [read $fd]
close $fd
# Open and write output file.
set fd [open "outfile.txt" "w"]
puts -nonewline $fd $fdata
close $fd
Work with strings:
# Get length of string.
set len [string length $str]
# Convert to upper/lower case.
set upper [string toupper $str]
set upper [string tolower $str]
# Trim characters (default is spaces).
set trimmed [string trim $str]
# List of changes to make to a string as a list.
# Values in the first column are changed to the value
# in the second column.
set chgs {
abc def
ghi jkl
}
set newstr [string map $chgs $oldstr]
Work with regular expressions:
set text {
My name is Bob
Hello Bob
My name is Mary
Hello Mary
}
# Find all names found in the phrases "My name is XXX".
set matches [regexp -nocase -all -inline
↪{my\s+name\s+is\s+(\w+)} $text]
foreach {match submatch} $matches {
puts "Name: $submatch"
}
# Will output:
# Name: Bob
# Name: Mary
# Change "My name is XXX" to "Your name is XXX".
set newstr [regsub -nocase -all {my\s+name\s+is\s+(\w+)}
↪$text {Your name is \1}]
puts $newstr
# Will output:
# Your name is Bob
# Hello Bob
# Your name is Mary
# Hello Mary
Work with expressions:
set a 1
set sum [expr 99 + $a]
set lt [expr $a < 10]
set two 2
set four [$two << 1]
Work with lists:
set alist { 1 2 3 }
# Get first item in list:
set one [lindex $alist 0]
# Append item to list:
lappend alist 0
# Sort a list:
set slist [lsort $alist]
# alist is { 1 2 3 0 }
# slist is { 0 1 2 3 }
Execute external commands:
# Execute grep and put output in Tcl variable:
set result [exec grep string file.txt]
# Catch errors in execution:
if { [catch {exec grep string file.txt} results options] } {
puts "Error executing grep"
} else {
puts "Grep executed ok: $results"
}
This only skims the surface of Tcl's commands. For more information on what you can do with Tcl, see the full list of the available Tcl Commands.
Quite often when you see a reference to Tcl, it's written as Tcl/Tk. Tk is the GUI toolkit that is closely associated with Tcl, although Tk is usable from other languages (it comes with most Python distributions).
At the start of the article, I noted that the GUI that comes with git is written in Tcl.
So obviously, you can write some pretty sophisticated GUI applications with Tcl/Tk, but if you've ever run the git GUI, you probably weren't exactly "wowed" by its look (unless you long for the days of Motif).
But for some simple GUI tasks, like adding a small pop-up window to some of your command-line scripts, Tk can be quite useful. Note that the latest versions of Tk (in "Tcl-time" this means circa 2007) have themeable widgets, and with a bit of work, you can make your Tcl/Tk apps look a bit more modern.
As an example of Tk with Tcl, the following code creates a window with two radio buttons and an "ok" button.
When the "ok" button is checked, the "value" of the selected radio button is printed and the program exits:
set yesno -1
wm title . "Which do you like?"
wm geometry . 300x90
radiobutton .rb1 -variable yesno -value 1 -text "I like yes"
radiobutton .rb2 -variable yesno -value 0 -text "I like no"
button .ok -text "Ok" -command { puts $yesno; exit }
grid .rb1 -sticky nw
grid .rb2 -sticky nw
grid .ok
In Tk, the top-level window/widget is named .
,
and child widgets are named .child
.
So, for example, a button inside a frame would have
a name that looks like ".frame.button".
In the previous example:
wm
command sets some options on the
top-level window, the title and the size.
radiobutton .rb1 ...
command creates a
radio button named .rb1
.
The radio button displays the text given to the -text
option, and when it's selected, it sets the variable
given to the -variable
option to the value given
to the -value
option.
.rb2
.
-command
option is executed.
grid
commands place the buttons into a grid layout
(one widget per row in this case).
The -sticky
option sets the widget's alignment.
Valid widget alignment characters are:
Since the script is showing a window, at the end of the script, instead of exiting, the script waits for GUI events. The window should look something like Figure 2.
For more information on what you can do with Tk, see the full list of the available Tk Commands.
So now that you've seen some Tcl (and a bit of Tk), you're probably itching to try it out.
On Linux, there are two commands for running Tcl scripts:
tclsh
runs scripts that don't use Tk, and wish
runs scripts that use Tk:
$ tclsh my-script.tcl
$ wish my-tkscript.tcl
However, note that with many versions of Tcl, you also can use Tk commands
with tclsh
by including the command package require
Tk
in your script
before executing any Tk-related commands.
On Linux, Tcl is likely already installed.
If not, it should be found in a package named tcl
in your
distro's standard repositories.
Another option is to install the commercial distribution of Tcl from
ActiveState.
A third option is the open-source Tcl distribution called TclKit. TclKit has some interesting and useful features that I cover a bit more in the next section.
But before I get to that, I need to issue a warning about the next section. I'm going to use some trigger language that may cause discomfort to some readers. So if you're a sensitive type, take a Xanax before reading on.
TclKits are Tcl and Tcl/Tk distributions that are contained in a single file.
These distributions have a number of uses, and one of the places I've found them to be particularly useful is (and here's the unsettling language) on Windows.
In my day job, I work with Windows, and if you've ever had to write a script (aka batch file) on Windows, you know it can be painful.
Since Windows NT arrived, batch files can do quite a bit more than DOS batch files could, but it's still not very much fun.
I've used both Cygwin and MSYS2, but both of those are heavyweight options that aren't really suited to sending a "quick" script to somebody to run.
I considered using PowerShell, but I haven't yet convinced myself that dotnet for the command line is the way to go, and on top of that, my organization doesn't allow users to run Powershell scripts by default.
So, none of those options work for the sorts of simple tasks I wanted to automate and potentially share with others. And, this is where TclKits come in handy.
A TclKit allows me to write scripts that don't require me to pull my hair out and that I can distribute in a single file, even if the "script" itself is composed of multiple files and includes some additional data files.
And, if I want to distribute it to someone who doesn't even have the TclKit executable, I even can package my scripts into a custom TclKit and still send only one file (albeit a bit bigger than just some script files).
These distributions come in a file that's called a "Starkit". These Starkits contain the Tcl interpreter (optionally with Tk) and any required support files in a virtual filesystem contained inside the executable itself.
Furthermore, you can create your own Starkits and package your scripts and data files inside your custom Starkit and even include a copy of the Tcl interpreter in your Starkit to give yourself a single file executable.
To create your own Starkits, you need to download TclKit (tclkitsh and/or tclkit) and the the Starkit Developer's eXtension (sdx.kit).
To "wrap" your Tcl script into a kit:
$ ls
sdx.kit test.tcl
$ cat test.tcl
puts "Hello Tcl"
# Wrap test.tcl into test.kit.
# Note the Tcl interpreter is not included in the .kit file.
$ tclkitsh sdx.kit qwrap test.tcl
5 updates applied
$ ls
sdx.kit test.tcl test.kit
The sdx
command qwrap
puts your script into a kit,
which you can now run:
$ tclkitsh test.kit
Hello Tcl
Next you can unwrap your kit and see what's inside:
$ tclkitsh sdx.kit unwrap test.kit
5 updates applied
$ ls
sdx.kit test.tcl test.kit test.vfs
$ find test.vfs
test.vfs
test.vfs/lib
test.vfs/lib/app-test
test.vfs/lib/app-test/pkgIndex.tcl
test.vfs/lib/app-test/test.tcl
test.vfs/main.tcl
The new directory test.vfs contains the unwrapped contents of the kit (vfs stands for Virtual File System).
With an unwrapped Starkit, you now can wrap it into a new Starkit that also includes the Tcl interpreter, giving you a single file executable:
$ ls -la test.kit
-rwxr-xr-x ... 781 ... test.kit
# Make copy of tclkitsh to use as the runtime.
$ cp ~/bin/tclkitsh tclkitsh-runtime
# Wrap .vfs and tclkitsh runtime into a single file
$ tclkitsh sdx.kit wrap test -runtime tclkitsh-runtime
4 updates applied
This latest wrapping of the script now contains a copy of the Tcl interpreter, so you can run it directly (and distribute it to users that don't have a copy of tclkit):
$ ls -la tclkitsh-runtime test
-rwxr-xr-x ... 4421483 ... tclkitsh-runtime
-rwxr-xr-x ... 4425769 ... test
$ ./test
Hello Tcl
It's not too exciting so far, but now modify the test script as follows:
$ cat test.tcl
package provide app-test 1.0
package require starkit
puts "Hello Tcl"
set fname [file join $starkit::topdir payload.txt]
set fd [open $fname]
set fdata [read $fd]
puts "Contents of $fname:"
puts $fdata
Then create a new file in the VFS and rewrap your Starkit:
$ echo "Hello Tcl from VFS" >test.vfs/payload.txt
$ tclkitsh sdx.kit wrap test -runtime tclkitsh-runtime
4 updates applied
$ ./test
Hello Tcl
Oops, that didn't do anything. If you look back at the contents of the unwrapped Starkit, you'll notice that there's now a copy of your script in the Starkit. That's the one you need to modify:
$ mv test.tcl test.vfs/lib/app-test/
$ tclkitsh sdx.kit wrap test -runtime tclkitsh-runtime
4 updates applied
$ ./test
Hello Tcl
Contents of .../test/payload.txt:
Hello Tcl from VFS
So now, you've embedded a data file within your Starkit, opened it from the Tcl script that's wrapped inside the Starkit and read it like any normal file.
The only difference is the path you used to open it.
Note that on Linux, you can chmod +x sdx.kit
and put it somewhere
in your path and execute it directly.
In this whirlwind tour of Tcl, you've seen what the language looks like, some of the built-in commands, the GUI package named Tk, how to run Tcl scripts and the TclKit version of Tcl. It's lot to digest in one article, but I hope it's given you enough information to get started with Tcl, and I also hope it's garnered enough interest for you to want to give Tcl a try.