Tcl - How to read a nested variable from inside a procedure - tcl

I am trying to do the following:
set myvar_key something
set a myvar
proc test {} {
# I am expecting mylocal_var to be "something", but it errors
set mylocal_var [set ${::a}_key]
}
# The proc is called like this
test
Thanks in advance,
Pedro

In these sorts of scenarios, it tends to be easier to use upvar to make a local alias for a variable in another scope. Yes, you can do trickery with set and such, but it tends to be harder to use, especially in real code. Once you've done the upvar, the local name is just another (highly efficient) way of accessing the named variable in another scope.
set myvar_key something
set a myvar
proc test {} {
# The #0 is quoted *just* for Stack Overflow's highlighting!
upvar "#0" ${::a}_key mylocal_var
}
If you were willing to rearrange your variables, you could instead do:
set key(myvar) something
set a myvar
proc test {} {
global a key
set mylocal_var $key($a)
}
But that does change what the main variable is so it isn't suitable in all cases. And you can do hybrids:
set key(myvar) something
set a myvar
proc test {} {
upvar "#0" key($::a) mylocal_var
}

You're almost there, but just missed something. As your code already shows, a is in the global namespace, thus you need ::a.
Same is true for myvar_key, thus you need to do
set myvar_key something
set a myvar
proc test {} {
set mylocal_var [set ::${::a}_key]
puts $mylocal_var
}
test
prints "something"

Related

how to uplevel argument name(name2)?

I want to uplevel argument name kuku(pupu).
I tried to use the below but none of sthem succeeded.
global kuku(pupu)
uplevel kuku(pupu)
upvar kuku(pupu)
How can I up the var?
You should be able to create the variable from inside the proc using global or uplevel. For global specifically, you cannot pass the variable name like that if it is an array, you can only pass the array name, like so if the upper namespace is global:
proc foo {} {
global kuku
set kuku(pupu) "some value"
# anything else to do
return
}
foo
puts $kuku(pupu)
You could use uplevel a bit differently like this:
proc foo {} {
set kuku(pupu) "some value"
uplevel [list set kuku(pupu) $kuku(pupu)]
# anything else to do
return
}
foo
puts $kuku(pupu)
Using upvar would be yet a bit different:
proc foo {arrayName} {
upvar $arrayName newName
set newName(pupu) "some value"
# anything else to do
return
}
foo kuku
puts $kuku(pupu)
IMO:
using global is the simplest, but you need to remember it works on the array name only, for arrays
For uplevel, you have to think about what should execute in the upper namespace (in this case, I wanted set kuku(pupu) "some value" to happen in the upper namespace) and can be a little difficult to grasp maybe.
upvar creates an alias from a variable in an upper namespace and makes it accessible locally. The alias can have the same name, it's just a bit less confusing if you use a different name from the original. A little similar to global, it works on the array name only.

Tcl : pass all variables from main to procedure

In my tcl script there is a part of the code that is repeated a lot, so I want to make a procedure out of it.
The thing is this part uses dozens of variables, which I would like to avoid passing as arguments to the procedure. Is there a way to make all variables visible to the procedure? (Practically I want the "main" to branch to the procedure like a "goto" and then return and continue in main).
Edited: It does not need to be a procedure, feel free to suggest other ways to do this. The important part is not need to declare all variables/arguments passing from main to the function/procedure.
Example:
proc dummy_proc {} {
set var1 $var2
set var2 $var3
}
set var2 2
set var3 3
dummy_proc
puts "$var1 $var2"
# should print "2 3"
This is possible but generally not advisable due to the fact that it can make code harder to read (there's no direct indication where the variables come from or how variable values suddenly change). However in some cases this can reduce repetitive code.
Use upvar (https://www.tcl.tk/man/tcl8.6/TclCmd/upvar.htm):
proc dummy_proc {} {
upvar var1 v1
upvar var2 v2
upvar var3 v3
set v1 $v2
set v2 $v3
}
What upvar does is create a variable in local scope that references another variable in the caller's scope.
Alternatively you can also try using uplevel (https://www.tcl.tk/man/tcl8.6/TclCmd/uplevel.htm):
proc dummy_proc {} {
uplevel {
set var1 $var2
set var2 $var3
}
}
What uplevel does is similar to upvar but instead of creating variable references it actually executes code in the caller's scope. It's as if you temporarily go back to the caller function without returning, execute some code and come back. Because you execute code in the caller's scope all variables visible in the caller's scope is visible in the code you upleveled. Uplevel behaves almost like a macro instead of a function.
Use global
proc dummy_proc {} {
global var2 var3
set var1 $var2
set var2 $var3
}
set var2 2
set var3 3
dummy_proc
puts "$var1 $var2"
If you have a lot of globals you want to pass, you can use some foreach, but than you'll have to have a way to find them all.
e.g. all globals are called GLOBAL_<SOMEARG>
proc dummy_proc {} {
foreach glb [info globals GLOBAL_*] {
global $glb
}
...
}

How to effectively override a procedure-local variable in TCL

So I have the following situation:
$ ls -l
-r--r----- 1.tcl
-rw-rw---- 2.tcl
$ cat 1.tcl
proc foo {args} {
puts "$bar"
}
and I need to make 1.tcl print something other than "can't read \"bar\"". In a good programming language, the obvious solution would be
$ cat > 2.tcl
set -global bar "hello, world"
foo
What would be a reasonable workaround in TCL? Unfortunately the real foo is a long function that I can't really make a copy of or sed to a temporary file at runtime.
You can do this for your specific example
$ cat 2.tcl
source 1.tcl
set bar "Hello, bar!"
# add a "global bar" command to the foo procedure
proc foo [info args foo] "global bar; [info body foo]"
foo
$ tclsh 2.tcl
Hello, bar!
Clearly this doesn't scale very well.
If the variable is simply undefined, the easiest way would be to patch the procedure with a definition:
proc foo [info args foo] "set bar \"hello, world\" ; [info body foo]"
You can also accomplish this using a read trace and a helper command. This removes the problem I mentioned above, where local assignments destroy the value you wanted to inject.
The original procedure, with an added command that sets the local variable to a value which is later printed.
proc foo args {
set bar foobar
puts "$bar"
}
% foo
foobar
Create a global variable (it doesn't matter if the name is the same or not).
set bar "hello, world"
Create a helper command that gets the name of the local variable, links to it, and assigns the value of the global variable to it. Since we already know the name we could hardcode it in the procedure, but this is more flexible.
proc readbar {name args} {
upvar 1 $name var
global bar
set var $bar
}
Add the trace to the body of the foo procedure. The trace will fire whenever the local variable bar is read, i.e. something attempts to retrieve its value. When the trace fires, the command readbar is called: it overwrites the current value of the variable with the globally set value.
proc foo [info args foo] "trace add variable bar read readbar; [info body foo]"
% foo
hello, world
If one doesn't want to pollute the namespace with the helper command, one can use an anonymous function instead:
proc foo [info args foo] [format {trace add variable bar read {apply {{name args} {
upvar 1 $name var
global bar
set var $bar
}}} ; %s} [info body foo]]
Documentation:
apply,
format,
global,
info,
proc,
puts,
set,
trace,
upvar,
Syntax of Tcl regular expressions
source 1.tcl
try {
foo
} on error {err res} {
set einfo [dict get $res -errorinfo]
if { [regexp {no such variable} $einfo] } {
puts "hello, world"
return -code 0
} else {
puts $einfo
return -code [dict get $res -code]
}
}
Tcl's procedures do not resolve variables to anything other than local variables by default. You have to explicitly ask for them to refer to something else (e.g., with global, variable or upvar). This means that it's always possible to see at a glance whether non-local things are happening, and that the script won't work.
It's possible to override this behaviour with a variable resolver, but Tcl doesn't really expose that API in its script interface. Some extensions do more. For example, it might work to use [incr Tcl] (i.e., itcl) as that does that sort of thing for variables in its objects. I can't remember if Expect also does this, or if that uses special-cased code for handling its variables.
Of course, you could get really sneaky and override the behaviour of proc.
rename proc real_proc
real_proc proc {name arguments body} {
uplevel 1 [list real_proc $name $arguments "global bar;$body"]
}
That's rather nasty though.

What purpose does upvar serve?

In the TCL code that I currently work on, the arguments in each procedure is upvar'ed to a local variable so to speak and then used. Something like this:
proc configure_XXXX { params_name_abc params_name_xyz} {
upvar $params_name_abc abc
upvar $params_name_xyz xyz
}
From here on, abc and xyz will be used to do whatever. I read the upvar TCL wiki but could not understand the advantages. I mean why cant we just use the variables that have been received as the arguments in the procedure. Could anybody please elaborate?
I mean why cant we just use the variables that have been received as the arguments in the procedure.
You can. It just gets annoying.
Typically, when you pass the name of a variable to a command, it is so that command can modify that variable. The classic examples of this are the set and incr commands, both of which take the name of a variable as their first argument.
set thisVariable $thisValue
You can do this with procedures too, but then you need to access the variable from the context of the procedure when it is a variable that is defined in the context of the caller of the procedure, which might be a namespace or might be a different local variable frame. To do that, we usually use upvar, which makes an alias from a local variable to a variable in the other context.
For example, here's a reimplementation of incr:
proc myIncr {variable {increment 1}} {
upvar 1 $variable v
set v [expr {$v + $increment}]
}
Why does writing to the local variable v cause the variable in the caller's context to be updated? Because we've aliased it (internally, it set up via a pointer to the other variable's storage structure; it's very fast once the upvar has been done). The same underlying mechanism is used for global and variable; they're all boiled down to fast variable aliases.
You could do it without, provided you use uplevel instead, but that gets rather more annoying:
proc myIncr {variable {increment 1}} {
set v [uplevel 1 [list set $variable]]
set v [expr {$v + $increment}]
uplevel 1 [list set $variable $v]
}
That's pretty nasty!
Alternatively, supposing we didn't do this at all. Then we'd need to pass the variable in by its value and then assign the result afterwards:
proc myIncr {v {increment 1}} {
set v [expr {$v + $increment}]
return $v
}
# Called like this
set foo [myIncr $foo]
Sometimes the right thing, but a totally different way of working!
One of the core principles of Tcl is that pretty much anything you can do with a standard library command (such as if or puts or incr) could also be done with a command that you wrote yourself. There are no keywords. Naturally there might be some efficiency concerns and some of the commands might need to be done in another language such as C to work right, but the semantics don't make any command special. They all just plain commands.
The upvar command will allow you to modify a variable in a block and make this modification visible from parent block.
Try this:
# a function that will modify the variable passed
proc set_upvar { varname } {
upvar 1 $varname var
puts "var was $var\n"
set var 5
puts "var is now $var\n"
}
# a function that will use the variable but that will not change it
proc set_no_upvar { var } {
puts "var was $var\n"
set var 6
puts "var is now $var\n"
}
set foo 10
# note the lack of '$' here
set_upvar foo
puts "foo is $foo\n"
set_no_upvar $foo
puts "foo is $foo\n"
As it was mentioned in comment above, it is often used for passing function arguments by reference (call by reference). A picture costs a thousand words:
proc f1 {x} {
upvar $x value
set value 0
}
proc f2 {x} {
set x 0
}
set x 1
f1 x
puts $x
set x 1
f2 x
puts $x
will result in:
$ ./call-by-ref.tcl
0
1
With upvar we changed variable x outside of function (from 1 to 0), without upvar we didn't.

using variables assingned in the script inside the proc in TCL

i want to use the variable assigned outside (proc) to be used inside the proc . For example i tried the following thing
set a 10
proc myproc { } {
puts $a
}
myproc
I am expecting the above script to print 10 . But the above script is erroring out "can't read "a": no such variable"
I cannot pass $a as argument to script because i have lot such variables i want to use inside my proc inside my script . Could you please help me to solve this problem ?
Your help is appreciated
If the variable is declared at the same stack level as the call to myproc then you can do following in your proc:
upvar a a
like this:
set a 10
proc myproc { } {
upvar a a
puts $a
}
myproc
and then you can use $a locally in the procedure. The upvar command "links" a variable declared somewhere in the stack with a local variable. If the variable is declared more than 1 level deeper in the stack, thn you need to pass "2" to upvar, so it knows where to look for the variable:
upvar 2 a a
If you don't pass the "2" (or other value), the upvar assumes default lookup depth of 1.
You can read more details about upvar in Tcl documentation for that command.
If the variable a is always a global variable (declared at the script top level), then you can use:
global a
in your procedure, instead of upvar.
If you have namespaces you could always assign it there :
namespace eval blah {
variable a 10
}
proc blah::myproc { } {
variable a
puts $a
}
blah::myproc
This way you can avoid potential collisions with other global variables