Latest blog post
Sept 21, 2015 — Defining until in Tcl
Background
One of Tcl’s greatest strengths is the ability for the programmer to seamlessly create new control structures. In this example, we will define a new control structure called until. Until takes two parameters: the first is a test condidtion and the second is a script to run while the condition is not true. For example, if we wished to print all numbers from 1 to 10, we could write the following:
until {$i == 10} {puts $i; incr i 1}
This of course assumes that the variable i has already been initialized to some number.
Implementation
The first iteration and simplest form of our control structure, in pseudo-code, is as follows:
-
Take the given test condition and run it in the caller’s scope.
-
If the test is untrue
-
Take the given script and run it in the caller’s scope
-
Return to #1
-
-
Our first attempt at the code:
proc until {test body} {
# We run the test in the caller’s scope, but check its return value in this scope
if { ! [uplevel 1 $test] } {
uplevel 1 $body
}
}
Testing our procedure gives the following output:
set i 1
until {expr {$i == 10}} {puts $i; incr i 1}
> 1
> 2
We get two values back. The first, "1" is from our body-script’s put statement. The second, "2" is the return value of the incr command.
Now that we’re sure our until control structure’s process flow works for a single iteration, we’ll modify it to run in a loop-like fasion. We do this by simply replacing the inner if-statement with the built in while command:
proc until {test body} {
# We run the test in the caller’s scope, but check its return value in this scope
while { ! [uplevel 1 $test] } {
uplevel 1 $body
}
}
Again, we test:
set i 1
until {expr {$i == 10}} {puts $i; incr i 1}
> 1
> 2
> 3
> 4
> 5
> 6
> 7
> 8
> 9
We have one additional step which is to clean up the call signature to until:
until {expr {$i == 10}} {puts $i; incr i 1}
will become:
until {$i == 10} {puts $i; incr i 1}
To make this change, we move the call to expr from the test expression into our procedure:
proc until {test body} {
# We run the test in the caller’s scope, but check its return value in this scope
while { ! [uplevel 1 {expr $test}] } {
uplevel 1 $body
}
}
Calling our procedure, we get the following:
until {$i == 10} {puts $i; incr i 1}
> can't read "test": no such variable
The reason for this is because the uplevel call in our while loop runs the literal form {expr $test} instead of the expanded form {expr {$i == 10}}. This is due to the use of curly-braces.
If we replace the braces with quotes instead:
proc until {test body} {
# We run the test in the caller’s scope, but check its return value in this scope
while { ! [uplevel 1 “expr $test”] } {
uplevel 1 $body
}
}
We get another error:
until {$i == 10} {puts $i; incr i 1}
>invalid command name "“{expr"
The solution is actually much simpler than using braces or quotes. Uplevel concatenates all arguments together into a single list, and then performs its operation. That means we can do the following:
proc until {test body} {
# We run the test in the caller’s scope, but check its return value in this scope
while { ! [uplevel 1 expr $test] } {
uplevel 1 $body
}
}