Purpose: to discuss one of the few 'gotchas' in Tcl
Recently on news:comp.lang.tcl Stephen Owen come seeking a solution to this error:
When I attempt to run the thing, I get extra characters after close-brace while executing "if {$findMsg != ""} { set errMsg2 "" if {[catch {exec find $localFindPath -name $compoFile} errMsg2] == 0} { s ..." (procedure "prepFile" line 18)
After a variety of suggestions to Stephen, he reported back:
Nowhere have I seen it written that }else{ rather than } else { will cause the Tcl interpreter to choke [[...]]
The reason is, of course, that the Tcl parser is more sensitive to white space than, say, the C compiler. With no white spaces between the braces and keywords, Tcl treated the entire character string as a potential command.
W. Rösler: I don't even think that Tcl is more sensitive to white space than C. Consider, for example, a C function prototype:
int f(double x);
You can't leave the spaces away without messing up the C parser. Spaces are required in certain places, in Tcl as well as in C, since both have a "longest match" parsing rule. Of course, C and Tcl have radically different ideas of what characters may be included in a token.
From http://www.psg.com/~joem/tcl/faq.html :
Q.B16- How can I get quoted strings to work the way I want?
A long article dealing with the issues can be found at ftp://ftp.procplace.com/pub/tcl/alcatel/docs/README.programmer.gz See also, Tcl Quoting.
Here are some short answers:
Q. I'm trying to build up a command for later execution but am having trouble with variable values that include whitespace or special characters.
A. The safest way to build up commands is to use the list command so that you can keep track of the list structure. Avoid using double quotes because you can end up with an extra trip through the evaluator. We'll illustrate this with a command to create a button that prints out the label on the button when you click it.
Wrong answer #1:
button $myname -text $label -command "puts stdout $label"
Why? because if $label has whitespace then the puts command will be passed the wrong number of arguments. If $label has $ or [ ] characters, they will be interpreted instead of printed.
Good answer #2:
button $myname -text $label -command [list puts stdout $label]
Why? because list will properly quote the value of $label
Q. I'm trying to build up a command for later execution but am having trouble getting some variables to evaluate now, and some to evaluate later when the command is run.
A. The cleanest way to do this is to define a procedure that hides the use of the variables at run time, and then build up a call to that procedure using the list command as described previously. (You can even define the procedure on the fly. It will have global scope even it if is created within another procedure.)
Wrong answer #1:
button $myname -text $label -command \ [list puts stdout $ArrayOfDynamicStuff($label)]
Why? The array value will be substituted when the button is created, not later on when the button is clicked. Also, note that the command is executed at the global scope, so it is not necessary to include a "global ArrayOfDynamicStuff" in the command.
Wrong answer #2 (backquotes and list):
button $myname -text $label -command \ [list puts stdout \$ArrayOfDynamicStuff($label)]
Why? Here the list command and the backquote of $ are fighting with each other. The command ends up being something like:
puts stdout {$ArrayOfDynamicStuff(foo)}
which prevents the substitution of the value of the array element.
Dubious answer #3 (backquotes and double-quotes):
button $myname -text $label -command \ "puts stdout \$ArrayOfDynamicStuff($label)"
Why? This only works if the value of $label has no special characters or whitespace.
Clean answer #4 (proc):
proc doit { i } { global ArrayOfDynamicStuff puts stdout $ArrayOfDynamicStuff($i) } button $myname -text $label -command [list doit $label]
Why? Using little TCL procs for your button commands is a good habit because it eliminates most needs for fancy quoting, and it makes it easier to tweak the button command later on.
Clean answer #5 (using apply from TCL 8.5+):
button $myname -text $label \ -command [ list ::apply { { i } { puts stdout $::ArrayOfDynamicStuff($i) } } $label ]
Q. I'm trying to pass along a variable number of args to another procedure but I'm having trouble getting the $args to expand right.
A. Avoid using eval and double quotes because that results in an extra trip through the interpreter. The eval command will do a concat of its arguments if there are more than one, so that pretty much eliminates the need to group things with double quotes. Let's extend the button example:
Wrong answer #1:
proc mybutton { myname label args } { button $myname -text $label -command [list puts stdout $label] $args }
Why? All the extra arguments to mybutton are grouped into one list element that is but into the value of $args. However, the button command expects to see individual arguments, not a sub-list.
Wrong answer #2:
proc mybutton { myname label args } { eval "button $myname -text $label \ -command [list puts stdout $label] $args" }
Why? The double quotes allow expansion of $label as well as $args, so if $label has any whitespace, the button command will be malformed
Good answer #3:
proc mybutton { myname label args } { set cmd {button $myname -text $label -command [list puts stdout $label]} eval $cmd $args }
Why? Eval will first concatenate its two arguments and then run the result through the interpreter. Think of this as stripping off the outer curly braces from $cmd and $arg and making a single list with all the elements of both. $label will be evaluated exactly once, so the puts command will remain good, and whatever went into args will also be processed exactly one time.
Good answer #4:
proc mybutton { myname label args } { button $myname -text $label -command [concat puts stdout $label $args] }
The concat command "flattens" its arguments into a single list.
Good answer #5:
proc mybutton {w label args} { eval button \$w -text \$label -command [list puts $label] $args }
Why? eval will substitute $args, $w and $label will be substituted afterwards, and therefore work with whitespace in their values.
Q. Why do I get a syntax error in an if/while/for statement?
A. You may have written something like
wish: set foo bar wish: if {$foo == bar} {puts stdout bar} syntax error in expression "$foo == bar"
in which bar is interpreted as neither a string nor a variable, since strings as operands in expressions MUST be surrounded by double quotes or braces. The reason is that "bare (unquoted)" words in an expression are interpreted as mathematical functions -- and the syntax error is due to the lack of a function named "bar". See the expr wiki page.
Change to
wish: if {$foo == "bar"} {puts stdout bar}
or
wish: if {$foo == {bar}} {puts stdout bar}
always in expressions, depending on if you want expansion performed or not.
This is also why
while {"true"} {do something}
is a syntax error and
while {true} {do something}
is not.
Contributed by Jesper Blommaskog (mailto:[email protected] ), and Glenn Jackman
RS While the above is true, the canonical "endless" loop is
while 1 {do something}
because "1" (without braces or quotes) is the truth value expr returns.
RFox With regards to Stephen's complaint about }else{ vs. } else { ..it clearly states that in the n-Dekalogue
[3] Words. Words of a command are separated by white space (except for newlines, which are command separators).
I think the 'problem' is a conceptual one... in most other programming languages (not all), control constructs are part of the language syntax rather than just another command subject to the command parser rules. (LISP -like languages and Tcl is LISP like are exceptions to that rule). The fact that Tcl has two underlying philosophical points is a really important thing to stress to beginning Tcl-ers I think. These points (besides the n-dekalogue for whichever version you are using) are:
See also A parser's monolog - Arts and crafts of Tcl-Tk programming