How to Expand Your Command-Line Scripting Options with Tcl

Get started scripting with Tcl, the Tool Command Language—this actually is your father's Oldsmobile. By Mich Frazier

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:

  1. Provide a "drink from the firehose" introduction to Tcl.
  2. Take a quick look at some of the commands that are built in to Tcl.
  3. Take an even quicker look at the Tk toolkit.
  4. Show how to run Tcl scripts and introduce TclKits.
  5. Explain how to use TclKit to create single file applications containing scripts and data files.

The Tcl Language

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:

  1. Variable substitution (for example, $name is replaced with the value of the variable name).
  2. Command substitution (for example, [cname...] is replaced with the return value of the command cname). This is akin to Bash's backticks or its $(...) syntax.
  3. Backslash substitution (for example, \<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 WORDs can be curly-brace blocks that span lines, if commands look like they should. So the following if command:


if { ... } {
    ...
} else {
    ...
}

Consists of five WORDs:

  1. The literal if.
  2. The if condition between braces { ... }.
  3. The "then" code block {\n ... \n}.
  4. The literal else.
  5. The 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.

Some Tcl Commands

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.

The Tk Toolkit

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).

Alt Tag Name

Figure 1. Git GUI

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:

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.

Alt Tag Name

Figure 2. Example Window

For more information on what you can do with Tk, see the full list of the available Tk Commands.

Running Tcl

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.

TclKit—the Batteries-Included Tcl Distribution

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.

Conclusion

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.

Resources

About the Author

Mitch Frazier is an embedded systems programmer at Emerson Electric Co. Mitch has been a contributor to and a friend of Linux Journal since the early 2000s.