Reputation: 8939
I have encountered a strange behaviour by the eval
command that I can't understand. When I try use eval to run a command whose name is stored in a variable I get strange results.
The array mCallBackCont
has the value ::postLayRep1||mainTableView sendToLoads
under insert
, so that*:
>>set mCallBackCont(insert)
::postLayRep1||mainTableView sendToLoads
Where ::postLayRep1||mainTableView
is an object of a class that has a sendToLoads
public method
When I try to do one of the following:
eval {::postLayRep1||mainTableView sendToLoads}
eval ::postLayRep1||mainTableView sendToLoads
eval "::postLayRep1||mainTableView sendToLoads"
eval $mCallBackCont(insert)
eval "mCallBackCont(insert)"
I get the correct behaviour, but When I use
eval {$mCallBackCont(insert)}
I get the error:
invalid command name "::postLayRep1||mainTableView sendToLoads"
When I try the same with a regular proc with no argument:
>>proc test_proc {} {return}
>>set a test_proc
>>eval {$a}
Everything works, but when I add argument, the same happens:
>>proc test_proc {val} {puts $val}
>>set a [list test_proc 1]
test_proc 1
>>eval {$a}
invalid command name "test_proc 1"
Since the eval command is part of a code from a library I'm using I can't change it, the only thing I can determine is the value of mCallBackCont(insert)
. The code in the library is:
if { [catch {eval {$mCallBackCont(insert) [namespace tail $this] $type $name $n $redraw}} e] } {
error "Wrong number of arguments for the procedure \"$mCallBackCont(insert)\". Should be \"table type name num redraw\"."
}
Why does eval {$var}
works for procs but not for methods of a class (I guess it somehow relates to the fact that the proc is a 1-word command, while a method is more complicated)?
In what way can I set the value of mCallBackCont(insert)
so that it works correctly?
* - I have tried to put the value in mCallBackCont(insert)
both a list and as one string surrounded by ""
Upvotes: 1
Views: 290
Reputation: 137787
First off, Tcl command names can have spaces in. Or in fact nearly any other character; the only ones you have to be careful of are :
s because ::
is a namespace separator, and a leading ::
is fine as it just means that it is a fully-qualified name.
Because of that, ::postLayRep1||mainTableView sendToLoads
is a legal but unusual command name. If you put the name in a variable, you can then use a read from that variable just as if it is a command name:
$theVariableContainingTheCommandName
Using an array element is nothing special in this regard.
Now, if you want to instead treat that as a script, you pass it to eval
like this:
eval $theVariableContainingTheScript
The real problem you're having is that you instead are doing:
eval {$theVariableContainingTheScript}
That is defined to be exactly identical to doing just:
$theVariableContainingTheScript
That won't ever do what you appear to want. Looking at the code that's causing you the problem:
if { [catch {eval {$mCallBackCont(insert) [namespace tail $this] $type $name $n $redraw}} e] } {
error "Wrong number of arguments for the procedure \"$mCallBackCont(insert)\". Should be \"table type name num redraw\"."
}
In this case, the value in the variable has to be the name of a command, and not just a script fragment. The simplest fix for you is to create an alias which binds in the extra arguments:
interp alias {} callBackForInsert {} ::postLayRep1||mainTableView sendToLoads
Then you can use callBackForInsert
just as if it was the combination of calling ::postLayRep1||mainTableView
with the first argument sendToLoads
. It's effectively a named partial application. Alternatively, you can use a helper procedure:
proc callBackForInsert args {
return [uplevel 1 {::postLayRep1||mainTableView sendToLoads} $args]
}
But that's both uglier and slower in this simple case. Better in 8.6 is to use tailcall
:
proc callBackForInsert args {
tailcall ::postLayRep1||mainTableView sendToLoads {*}$args
}
But that is still slower than an alias because of the overhead of the extra stack frame manipulation.
However, the nicest fix if you can is to alter the library so it uses the callback like this (assuming Tcl 8.5 or later):
if { [catch {eval {{*}$mCallBackCont(insert) [namespace tail $this] $type $name $n $redraw}} e] } {
error "Wrong number of arguments for the procedure \"$mCallBackCont(insert)\". Should be \"table type name num redraw\"."
}
Which can be simplified to:
if { [catch {{*}$mCallBackCont(insert) [namespace tail $this] $type $name $n $redraw} e] } {
error "Wrong number of arguments for the procedure \"$mCallBackCont(insert)\". Should be \"table type name num redraw\"."
}
A good rule of thumb is that there's hardly ever a reason to use eval
in modern Tcl code; a {*}
-expansion is virtually always closer to what is intended.
If you're stuck with 8.4 but can change the library code, you can do this instead:
if { [catch {eval $mCallBackCont(insert) {[namespace tail $this] $type $name $n $redraw}} e] } {
error "Wrong number of arguments for the procedure \"$mCallBackCont(insert)\". Should be \"table type name num redraw\"."
}
This uses the fact that eval
will concatenate its arguments before feeding them back through the Tcl script evaluation engine.
The combination of aliases, expansion, tailcall
and (not used in this answer) ensembles lets you do awesome stuff with very little code, allowing complicated remixing of arguments without having to load yourself down with lots of helper procedures.
Upvotes: 4