tcl changing a global variable while in a child interpreter (interp) - tcl

I am trying to use a TCL program to read TCL modulefiles and translate them into another language. This has worked quite well until now. For reasons too complicated explain I have to treat "puts stderr" differently in different parts of the modulefile. I am asking for help in trying to figure out a way to do this.
Below is an extremely abbreviated modulefile called "modfile". This "modfile" is translated or "sourced" by a second tcl program.
proc ModulesHelp { } {
 puts stderr "(1) This is a help message"
}
puts stderr "(2) Here in modfile"
The puts statement inside ModulesHelp has to be treated differently from the second puts statement. Note that any solution CAN NOT CHANGE "modfile". That file is not under my control.
Here is my attempt at a solution:
#!/usr/bin/env tclsh
proc myPuts { stream msg } {
global putMode
puts stdout "putMode: $putMode" # <====== HERE 1
puts stdout $msg
}
proc report { message } {
puts stderr "$message"
}
proc execute-modulefile { m } {
global MODFILE putMode
set putMode "normal"
set slave "__mod"
interp create $slave
interp alias $slave puts {} myPuts
interp alias $slave report {} report
interp eval $slave {global putMode }
interp eval $slave [list "set" "putMode" $putMode]
interp eval $slave [list "set" "m" $m]
set errorVal [interp eval $slave {
set sourceFailed [catch {source $m } errorMsg]
if {[info procs "ModulesHelp"] == "ModulesHelp" } {
set putMode "InHelp" # <======= HERE 2
ModulesHelp
}
if {$sourceFailed} {
report $errorMsg
return 1
}
}]
interp delete $slave
return $errorVal
}
eval execute-modulefile $argv
To run this I do: $ ./try.tcl modfile where obviously the above script is "try.tcl" and the modulefile is "modfile". I am running this on a linux system with tcl 8.4.
What I would like to have happen is that at the line labelled "HERE 2" I like to somehow change the global variable of "putMode" from "normal" to "InHelp" so that I can change the behavior at the line labelled "HERE 1". No matter what I have tried to do I can not change the value of putMode at "HERE 1" by doing something at "HERE 2". The puts statement at "HERE1 always says "normal".
Using a global variable seems like the easiest solution but if someone could show me how to use namespaces or some other technique, I'll be happy with that as well.
Thanks for any insight.
I greatly appreciate the time that others have looked at my question. I am trying to use the proposed solution and I'm not quite seeing it. Here is my new attempt at a solution (This doesn't work at all). Can someone suggest how I modify this code to change "putMode" to inHelp where "HERE 2" is? Also is there something special that needs to go where "HERE 1" is?
#!/usr/bin/env tclsh
proc myPuts { stream msg } {
global putMode
puts stdout "putMode: $putMode" # <=== HERE 1
puts stdout $msg
}
proc report { message } {
puts stderr "$message"
}
proc PutModeTrace {childInterp operation realPutMode} {
# Alias the main array element for the purposes of this procedure
upvar \#0 PutMode($childInterp) realPutMode
if {$operation eq "read"} {
interp eval $childInterp [list set putMode $realPutMode]
} elseif {$operation eq "write"} {
set realPutMode [interp eval $childInterp {set putMode}]
}
}
proc execute-modulefile { m } {
global MODFILE putMode
set putMode "normal"
set slave [interp create]
interp alias $slave puts {} myPuts
interp alias $slave report {} report
interp eval $slave {global putMode }
interp eval $slave [list "set" "putMode" $putMode]
interp eval $slave [list "set" "m" $m]
interp eval $slave [list "set" "slave" $slave]
interp eval $slave {trace add variable putMode {read write} PutModeTrace}
interp alias $slave PutModeTrace {} PutModeTrace $slave
set errorVal [interp eval $slave {
set sourceFailed [catch {source $m } errorMsg]
if {[info procs "ModulesHelp"] == "ModulesHelp" } {
set start "help(\[\["
set end "\]\])"
PutModeTrace $slave "write" "inHelp" # <=== HERE 2
puts stdout $start
ModulesHelp
puts stdout $end
}
if {$sourceFailed} {
report $errorMsg
return 1
}
}]
interp delete $slave
return $errorVal
}
eval execute-modulefile $argv

The problem is that the slave and the master are different interpreters. This means that every interpreter has it's own
commands
variables
namespaces
channels
You can't simply change a variable in the master from the slave, so the easiest solution would be:
interp alias $slave InHelp {} set ::putMode InHelp
and calling this alias instead.
Some other notes:
An other option would be to change the puts alias when InHelp is called. Example
proc InHelp {slave} {
interp alias $slave puts {} HelpPuts
}
and using it with interp alias $slave {} InHelp $slave
You don't have to assign a name for the slave. Just do
set slave [interp create]
Single words don't have to be quoted, so
list "a" "b" "c"
is equal to
list a b c
If you need argument expansion (and use at least Tcl 8.5) use {*}$argv instead eval.
But because execute-modfile only accept one argument, execute-modfile [lindex $argv 0] should do the job.

As Johannes writes, variables are entirely separate in different interpreters; they're not shared at all.
However, you can use trace and some aliases to couple things together. I'll show how to do it for a simple scalar variable (with the parent having an array of them, presumably one for each child interpreter), under the assumption that you never want to have the setting of the variable in the master interpreter trigger a trace in the child.
interp eval $child {trace add variable putMode {read write} PutModeTrace}
interp alias $child PutModeTrace {} PutModeTrace $child
proc PutModeTrace {childInterp varName elementName operation} {
# Ignore the elementName argument
# Alias the main array element for the purposes of this procedure
upvar \#0 PutMode($childInterp) realPutMode
if {$operation eq "read"} {
interp eval $childInterp [list set putMode $realPutMode]
} elseif {$operation eq "write"} {
set realPutMode [interp eval $childInterp {set putMode}]
}
}
This makes it so that whenever the child interpreter reads or writes the putMode variable, the read/write gets reflected into the master.
It's easier to map a command (via an alias) though, and if you were using Tcl 8.6 I'd suggest stacking and unstacking custom transformations on stderr instead. (But that's a massively more sophisticated technique.)

Thanks for all the help. It has taken me a while to understand what was being proposed. Here is the code that does what I want:
#!/usr/bin/env tclsh
proc myPuts { stream msg } {
global putMode
if {$putMode != "inHelp"} {
puts stderr $msg
} else {
puts stdout $msg
}
}
proc report { message } {
puts stderr "$message"
}
proc setPutMode { value } {
global putMode
set putMode $value
}
proc execute-modulefile { m } {
global MODFILE putMode
set putMode "normal"
set slave [interp create]
interp alias $slave puts {} myPuts
interp alias $slave setPutMode {} setPutMode
interp alias $slave report {} report
interp eval $slave {global putMode }
interp eval $slave [list "set" "putMode" $putMode]
interp eval $slave [list "set" "m" $m]
interp eval $slave [list "set" "slave" $slave]
interp eval $slave {trace add variable putMode {read write} PutModeTrace}
interp alias $slave PutModeTrace {} PutModeTrace $slave
set errorVal [interp eval $slave {
set sourceFailed [catch {source $m } errorMsg]
if {[info procs "ModulesHelp"] == "ModulesHelp" } {
set start "help(\[\["
set end "\]\])"
setPutMode "inHelp"
puts stdout $start
ModulesHelp
puts stdout $end
setPutMode "normal"
}
if {$sourceFailed} {
report $errorMsg
return 1
}
}]
interp delete $slave
return $errorVal
}
eval execute-modulefile $argv
Thanks again.

Related

TCL: Redirect output of proc to a file

I need to redirect output of a proc to a file. The "redirect" command isn't working for the Tcl interpreter that my tool uses. So I'm trying "exec echo [proc_name]" instead, which was suggested in one of the threads on this site. But this doesn't work, the file ${dump_dir}/signal_list.txt comes out empty,
proc dump_signals {{dump_dir "."}} {
upvar build build
puts "Dumping signals.."
set my_file [open ${dump_dir}/signal_list.txt w]
exec echo [get_signals] > $my_file
}
'get_signals' is a proc, which calls another proc,
proc puts_name_and_value {name} {
set value [value %h $name]
puts "$name $value"
}
proc get_signals {} {
# Get list of signals
set signal_list {test.myreg test.myreg2}
foreach signal $signal_list {
puts_name_and_value $signal
}
}
My workaround for now is this, writing to the file in the bottom level proc by upvar'ing the file variable. This works but isn't the most clean way of doing this. Please let me know how to cleanly redirect the output of a proc to a file.
proc puts_name_and_value {name} {
upvar my_file my_file
set value [value %h $name]
puts $my_file "$name $value"
#puts "$name $value"
}
proc get_signals {} {
upvar my_file my_file
# Get list of signals
set signal_list {test.myreg test.myreg2}
foreach signal $signal_list {
puts_name_and_value $signal
}
}
proc dump_signals {{dump_dir "."}} {
upvar build build
puts "Dumping signals.."
set my_file [open ${dump_dir}/signal_list.txt w]
get_signals
}
Your dump_signals proc writes to standard output, and doesn't return anything. So of course trying to use a shell to redirect its output to a file isn't going to result in anything in the file.
One solution is to use tcl's transchan API with chan push and chan pop to temporarily override what stdout goes to. Example:
#!/usr/bin/env tclsh
proc redirector {fd args} {
switch -- [lindex $args 0] {
initialize {
# Sanity check
if {[lindex $args 2] ne "write"} {
error "Can only redirect an output channel"
}
return {initialize write finalize}
}
write {
puts -nonewline $fd [lindex $args 2]
}
finalize {
close $fd
}
}
}
proc writer_demo {} {
puts "line one"
puts "line two"
}
proc main {} {
chan push stdout [list redirector [open output.txt w]]
writer_demo
chan pop stdout
}
main
Running this script will produce a file output.txt with the contents of writer_demo's puts calls instead of having them go to standard output like normal.
You could also just pass the file handle to write to as an argument to your functions (Instead of using upvar everywhere):
proc puts_name_and_value {out name} {
set value [value %h $name]
puts $out "$name $value"
}
proc get_signals {{out stdout}} {
# Get list of signals
set signal_list {test.myreg test.myreg2}
foreach signal $signal_list {
puts_name_and_value $out $signal
}
}
proc dump_signals {{dump_dir "."}} {
upvar build build
puts "Dumping signals.."
set my_file [open ${dump_dir}/signal_list.txt w]
get_signals $my_file
}

Renaming puts in namespace causes issue in TCL

When I am trying to rename the puts command inside name-space, It causes the problem. I am renaming puts because I don't want to display the echo statement of particular procedure.
namespace eval temp {
namespace export print_proc
proc replacement_puts args {}
proc silentEval {script} {
rename puts original_puts
interp alias {} puts {} temp::replacement_puts
catch [list uplevel 1 $script] msg opts
rename puts {}
rename original_puts puts
return -options $opts $msg
}
proc print_proc {} {
puts "before call"
silentEval {a}
puts "aftter call"
}
proc a {} {
puts "inner call"
}
}
package provide temp 1.0
In Example, I don't want to display the echo statement of proc a.
But after execution, It shows error that Invalid Command Name "puts"
Thanks
Glenn Jackman's answer should solve your problem. I'd just like to point out that you don't really need to rename anything. With this definition:
namespace eval temp {
namespace export print_proc
proc puts args {}
proc silentEval script {
catch [list uplevel 1 $script] msg opts
return -options $opts $msg
}
proc print_proc {} {
::puts "before call"
silentEval a
::puts "aftter call"
}
proc a {} {
puts "inner call"
}
}
when a is invoked and it calls puts, it will actually invoke the ::temp::puts command in preference to the global puts -- in effect, the namespace puts overrides the global puts. In print_proc we want the global puts to be invoked, so we add :: before the name.
Of course, all this goes for scripts defined within ::temp, like in the example.
(There isn't much point in calling catch if you're just going to re-throw the exception, but I suppose this is just placeholder code.)
Documentation: catch, list, namespace, proc, puts, return
You just have to be explicit that you're altering the global puts
namespace eval temp {
proc silentEval {script} {
rename ::puts ::original_puts
proc ::puts args {}
catch [list uplevel 1 $script] msg opts
rename ::puts {}
rename ::original_puts ::puts
return -options $opts $msg
}
}

Way to list all procedures in a Tcl file

Is there any way to list all the procedures(proc) in a myFile.tcl using another tcl file or in the same file.
You can use [info procs] before and after sourcing the file in question and compare the results to determine which procs were added. For example:
proc diff {before after} {
set result [list]
foreach name $before {
set procs($name) 1
}
foreach name $after {
if { ![info exists procs($name)] } {
lappend result $name
}
}
return [lsort $result]
}
set __before [info procs]
source myFile.tcl
set __after [info procs]
puts "Added procs: [diff $__before $__after]"
One thing I like about this solution is that the diff procedure is really just a generic set differencing utility -- it's not specific to comparing lists of defined procedures.
The cheapest way is to just open the file and use regexp to pick out the names. It's not perfectly accurate, but it does a reasonably good job.
set f [open "sourcefile.tcl"]
set data [read $f]
close $f
foreach {dummy procName} [regexp -all -inline -line {^[\s:]*proc (\S+)} $data] {
puts "Found procedure $procName"
}
Does it deal with all cases? No. Does it deal with a useful subset? Yes. Is the subset large enough for you? Quite possibly.
Yes it is, although not that easy. The basic idea is to source the file in a modified slave interp that only executes some commands:
proc proc_handler {name arguments body} {
puts $name
}
set i [interp create -safe]
interp eval $i {proc unknown args {}}
interp alias $i proc {} proc_handler
interp invokehidden source yourfile.tcl
This approach will fail if the file requires other packages (package require will not work), relies on the result of some usually auto_load'ed commands etc..
It also does not take namespaces into account. (namespace eval ::foo {proc bar a {}} creates a proc with the name ::foo::bar
For a more complex implementation you could look into auto.tcl's auto_mkindex, which has a similar goal.
Here is a different approach:
Create a temporary namespace
Source (include) the script in question, then
Use the info procs command to get a list of procs
Delete the temporary namespace upon finish
Here is my script, *list_procs.tcl*:
#!/usr/bin/env tclsh
# Script to scan a Tcl script and list all the procs
proc listProcsFromFile {fileName} {
namespace eval TempNamespace {
source $fileName
set procsList [info procs]
}
set result $::TempNamespace::procsList
namespace delete TempNamespace
return $result
}
set fileName [lindex $::argv 0]
set procsList [listProcsFromFile $fileName]
puts "File $fileName contains the following procs: $procsList"
For example, if you have the following script, procs.tcl:
proc foo {a b c} {}
proc bar {a} {}
Then running the script will produce:
$ tclsh list_procs.tcl procs.tcl
File procs.tcl contains the following procs: foo bar

how to declare a variable globally which is used only in proc

i am having a following code:
proc testList {setupFile ""} {
if {$setupFile == ""} {
set setupFile location
}
}
proc run {} {
puts "$setupFile"
}
I am getting syntax error. I know if i declare the setupFile variable outside the proc i.e in the main proc then i can append it with namespace say ::65WL::setupFile to make it global but not getting how to do that if a variable itself is defined in the proc only.
You can refer to the global namespace with ::.
proc testList {{local_setupFile location}} {
# the default value is set in the arguments list.
set ::setupFile $local_setupFile
}
proc run {} {
puts $::setupFile
}
Tcl variables that are not local to a specific procedure run need to be bound to a namespace; the namespace can be the global namespace (there's a special command for that) but doesn't need to be. Thus, to have a variable that is shared between two procedures, you need to give it an exposed name:
proc testList {{setup_file ""}} {
# Use the 'eq' operator; more efficient for string equality
if {$setup_file eq ""} {
set setup_file location
}
global setupFile
set setupFile $setup_file
}
proc run {} {
global setupFile
puts "$setupFile"
}
Now, that's for sharing a full variable. There are some other alternatives provided you only want to share a value. For example, these two possibilities:
proc testList {{setup_file ""}} {
if {$setup_file eq ""} {
set setup_file location
}
# Create a procedure body at run-time
proc run {} [concat [list set setupFile $setup_file] \; {
puts "$setupFile"
}]
}
proc testList {{setup_file ""}} {
if {$setup_file eq ""} {
set setup_file location
}
# Set the value through combined use of aliases and a lambda term
interp alias {} run {} apply {setupFile {
puts "$setupFile"
}} $setup_file
}
There are more options with Tcl 8.6, but that's still in beta.
you can use uplevel, upvar and/or global
proc testList {{setupFile ""}} {
if {$setupFile eq ""} {
set setupFile location;
uplevel set setupFile $setupFile;
}
}
proc run {} {
upvar setupFile setupFile;
puts "$setupFile";
}
or
proc run {} {
global setupFile;
puts "$setupFile";
}
the first is prefered.

Unable to pass a variable to a procedure using upvar in Tcl

I need a procedure that will be able to access, read and change a variable from the namespace of the caller. The variable is called _current_selection. I have tried to do it using upvar in several different ways, but nothing worked. (I've written small test proc just to test the upvar mechanism). Here are my attempts:
call to proc:
select_shape $this _current_selection
proc:
proc select_shape {main_gui var_name} {
upvar $var_name curr_sel
puts " previously changed: $curr_sel"
set curr_sel [$curr_sel + 1]
}
For my second attempt:
call to proc:
select_shape $this
proc:
proc select_shape {main_gui} {
upvar _current_selection curr_sel
puts " previously changed: $curr_sel"
set curr_sel [$curr_sel + 1]
}
In all the attempts, once it reaches this area in the code it says can't read "curr_sel": no such variable
What am I doing wrong?
EDIT:
The call for the function is made from a bind command:
$this/zinc bind current <Button-1> [list select_shape $this _current_selection]
at start I thought that it doesn't matter. but maybe It does.
I believe that bind commands operate in the global namespace, so that's where the variable is expected to be found. This might work:
$this/zinc bind current <Button-1> \
[list select_shape $this [namespace current]::_current_selection]
for upvar to work the variable must exist in the scope that you are calling it in. consider the following:
proc t {varName} {
upvar $varName var
puts $var
}
#set x 1
t x
If you run it as it is you'll get the error you are reporting, uncomment the set x 1 line and it will work.
In the example below I've tried to cover the most variants of changing variables from other namespace. It 100% works for me. Maybe it will help.
proc select_shape {main_gui var_name} {
upvar $var_name curr_sel
puts " previously changed: $curr_sel"
incr curr_sel
}
namespace eval N {
variable _current_selection 1
variable this "some_value"
proc testN1 {} {
variable _current_selection
variable this
select_shape $this _current_selection
puts " new: $_current_selection"
}
# using absolute namespace name
proc testN2 {} {
select_shape [set [namespace current]::this] [namespace current]::_current_selection
puts " new: [set [namespace current]::_current_selection]"
}
select_shape $this _current_selection
puts " new: $_current_selection"
}
N::testN1
N::testN2
#-------------------------------------
# Example with Itcl class
package require Itcl
itcl::class C {
private variable _current_selection 10
public method testC {} {
select_shape $this [itcl::scope _current_selection]
puts " new: $_current_selection"
}
}
set c [C #auto]
$c testC