Accessing variables in TCL across scopes - tcl

I'm trying to learn tcl scripting. My req is very simple. I need to access the array "args" in the second if condition in the for loop. I tried the code below. Since "argv" scope is limited to second if condition, it is NOT accessible in for loop
Then I tried declaring argv as global var -
array set args {}
right below the ned of first if condition. Even after declaring "args" as global array did NOT help.
How do I access the variable in the cope of second if contion, in the for loop below ?
if {$argc != 4} {
puts "Insufficient arguments"
exit 1
}
if { $::argc > 0 } {
set i 1
foreach arg $::argv {
puts "argument $i is $arg"
set args(i) arg
incr i
}
} else {
puts "no command line argument passed"
}
for {set x 0} { $x<2 } {incr x} {
puts "Arrray: [lindex $args $x]"
}

For your original code, this is the error I get:
can't read "args": variable is array
while executing
"lindex $args $x"
("for" body line 2)
invoked from within
"for {set x 0} { $x<2 } {incr x} {
puts "Arrray: [lindex $args $x]"
}"
(file "main.tcl" line 20)
In Tcl, arrays are not lists. You have to write
for {set x 0} { $x<2 } {incr x} {
puts "Arrray: $args($x)"
}
But then I get this:
can't read "args(0)": no such element in array
while executing
"puts "Arrray: $args($x)""
("for" body line 2)
invoked from within
"for {set x 0} { $x<2 } {incr x} {
puts "Arrray: $args($x)"
}"
(file "main.tcl" line 20)
Well there's several problems here. You're setting array elements starting with index 1 but reading them starting with index 0. So let's correct that to 0 everywhere:
set i 0
But also you're missing some $'s in the setting of the elements:
set args($i) $arg
That looks better. Final code:
if {$argc != 4} {
puts "Insufficient arguments"
exit 1
}
if { $::argc > 0 } {
set i 0
foreach arg $::argv {
puts "argument $i is $arg"
set args($i) $arg
incr i
}
} else {
puts "no command line argument passed"
}
for {set x 0} { $x<2 } {incr x} {
puts "Arrray: $args($x)"
}
So, scope wasn't quite the issue. You're getting there though!

Tcl does not import globals by default. You need to import your globals:
global args
set args(i) arg
Some people prefer to import globals at the top of the proc:
global args
if {$argc != 4} {
puts "Insufficient arguments"
exit 1
}
if { $::argc > 0 } {
set i 1
....
See: https://www.tcl.tk/man/tcl8.7/TclCmd/global.htm
Alternatively, you can directly access the global namespace, in fact you're already using that syntax with ::argc:
set ::args(i) arg

Related

Namespaces and procedures and scope inside namespaces

I'm trying to make a "safe" method of generating request ids for web sockets (just a desktop app not a real server) and want each socket to have its own id generator. All I'm doing is generating ids and recycling them after the request completes, such that the id doesn't grow unlimited throughout a user's session. I used an example concerning closures for a counter in JavaScript from David Flanagan's book and all seems to work well in Tcl but I'd greatly appreciate any advice on how to do this correctly and how I can test that these variables cannot be altered by the main program apart from calling one of the procedures within the namespaces. For example, is it possible to modify the gap list under the WEBS::$sock from the global namespace with [meant without] calling one of the procedures? Thank you.
Also, is there any difference between declaring namespace eval WEBS {} outside proc. ReqIdGenerator and using namespace eval WEBS::$sock inside the procedure? I can see that the results are the same for my little tests but wondered if there was any differences otherwise.
As an aside, in JS using the push and pop methods of arrays, it seems easier to recycle ids on a last-in-first-out basis; but using Tcl lists, it seems easier to use a first-in-first-out basis because using lassign with one variable assigns index 0 to the variable and returns the remaining elements as a new list. The equivalent of array.pop() seems to require more steps. Is that a correct observation? Thank you.
WARNING:
There is an error in this code in that the namespace references $sock and it works only because it is a global variable. If it were not global, the code would throw and error. The best I could find thus far is in this question.
proc ReqIdGenerator {sock} {
namespace eval WEBS {
namespace eval $sock {
variable max 0
variable gap {}
variable open {}
variable sock $sock
proc getId {} {
variable max
variable gap
variable open
if { [llength $gap] > 0 } {
set gap [lassign $gap id]
lappend open $id
return $id
} else {
lappend open [set id [incr max]]
return $id
}
chan puts stdout "Error in getId"
return -1
}
proc delId {id} {
variable max
variable gap
variable open
if { [set i [lsearch $open $id]] == -1 } {
return 1
} elseif { [llength $open] == 1 } {
reset
} else {
lappend gap [lindex $open $i]
set open [lreplace $open $i $i]
}
return 0
}
proc reset {} {
variable max 0
variable gap {}
variable open {}
}
proc getState {{prop "all"}} {
variable max
variable gap
variable open
variable sock
if { $prop eq "all" } {
return [list $max $gap $open]
} elseif { $prop eq "text" } {
return "State of socket $sock: max: $max; gap: $gap; open: $open"
} else {
return [set $prop]
}
}
}
}
}
set sock 123
ReqIdGenerator $sock
set sock 456
ReqIdGenerator $sock
# Add ids 1 through 10 to both sockets
for {set i 0} {$i<10} {incr i} {
WEBS::123::getId
WEBS::${sock}::getId
}
# Delete even ids from socket 456
for {set i 2 } {$i<11} {incr i 2} {
WEBS::${sock}::delId $i
}
# Delete odd ids from socket 123
for {set i 1 } {$i<10} {incr i 2} {
WEBS::123::delId $i
}
chan puts stdout [WEBS::123::getState text]
# => State of socket 123: max: 10; gap: 1 3 5 7 9; open: 2 4 6 8 10
chan puts stdout [WEBS::456::getState text]
# => State of socket 456: max: 10; gap: 2 4 6 8 10; open: 1 3 5 7 9
Lots of questions to unpack here.
how I can test that these variables cannot be altered by the main program apart from calling one of the procedures within the namespaces
You can't. There are no access controls within an interpreter. You can have multiple interpreters and there are strong access controls between them, but that's pretty heavyweight. However, it's conventional to not go rummaging around in a namespace that you don't own to peek at things you've not formally been told about on the grounds that they're liable to be changed at any moment without any sort of notification to you (usually not at runtime, but no guarantees!).
A phrase I've seen used in the community is "If you break it, you get to keep all the pieces".
For example, is it possible to modify the gap list under the WEBS::$sock from the global namespace with calling one of the procedures?
I'm sure it is. Finding it might be tricky, but once you have the name you can change it.
is there any difference between declaring namespace eval WEBS {} outside proc. ReqIdGenerator and using namespace eval WEBS::$sock inside the procedure?
There, assuming you handle the possible differences in name resolution scope of the name of the namespace itself. (That doesn't matter for fully qualified names — names beginning with :: — but relative names might resolve differently.)
The equivalent of array.pop() seems to require more steps. Is that a correct observation?
Yes. 8.7 adds lpop to address this weakness.
Your code appears to be reinventing objects. Use TclOO (or one of the other major object systems such as [incr Tcl] or XOTcl) for that; it's better at the job.
oo::class create ReqIdGenerator {
variable max gap open sock
constructor {sock} {
set max 0
set gap {}
set open {}
set [my varname sock] $sock; # messy because formal parameter
}
method getId {} {
if { [llength $gap] > 0 } {
set gap [lassign $gap id]
lappend open $id
return $id
} else {
lappend open [set id [incr max]]
return $id
}
chan puts stdout "Error in getId"
return -1
}
method delId {id} {
if { [set i [lsearch $open $id]] == -1 } {
return 1
} elseif { [llength $open] == 1 } {
my reset
} else {
lappend gap [lindex $open $i]
set open [lreplace $open $i $i]
}
return 0
}
method reset {} {
set max 0
set gap {}
set open {}
}
method getState {{prop "all"}} {
if { $prop eq "all" } {
return [list $max $gap $open]
} elseif { $prop eq "text" } {
return "State of socket $sock: max: $max; gap: $gap; open: $open"
} else {
return [set [my varname $prop]]
}
}
}
set sock 123
set s1 [ReqIdGenerator new $sock]
set sock 456
set s2 [ReqIdGenerator new $sock]
# Add ids 1 through 10 to both sockets
for {set i 0} {$i<10} {incr i} {
$s1 getId
$s2 getId
}
# Etc.

passing a list into a procedure in tcl

I want to pass a list into a procedure but not sure how to. I've looked at some examples of how to do this but the examples are too complicated and I don't understand them. The list and procedure are shown below.
set RS_CheckBoxes [list kl15_cb din1_cb din2_cb din3_cb din4_cb \
dinnc_cb ain1_cb ain2_cb ain3_cb ain4_cb a_cb \
b_cb u_cb v_cb w_cb sin_cb cos_cb th1_cb th2_cb hvil_cb]
tk::button .rs.enter -height 2 -text "Enter" -width 10 -command {x $RS_CheckBoxes}
proc x {$RS_CheckBoxes} {
if {$RS_CheckBoxes} {
puts "ON"
} else {
puts "OFF"
}
}
At present I'm using the below code but want to reduce the amount of lines.
tk::button .relSel.b -height 2 -text "Enter" -width 10 -command {if {$kl15_cb} {
puts "$kl15_cb"
} else {
puts "$kl15_cb"
}
if {$dinnc_cb} {
puts "$dinnc_cb"
} else {
puts "$dinnc_cb"
}
if {$din1_cb} {
puts "$din1_cb"
} else {
puts "$din1_cb"
}
if {......... etc}
............. etc
Your description is not entirely clear. Do you want to pass a list of global variable names into a proc and then print ON or OFF based on their boolean value?
Currently you just seem to be printing the value of the variables in a very complicated way.
if {$dinnc_cb} {
puts "$dinnc_cb"
} else {
puts "$dinnc_cb"
}
is equal to puts $dinnc_cb unless you want the code to throw an error when the value cannot be interpreted as a boolean.
If my understanding is correct, try this:
proc x {varlist} {
foreach varname $varlist {
upvar #0 $varname var
puts "$varname: [lindex {ON OFF} [expr {!$var}]]"
}
}
The upvar creates a link from the global variable in $varname to the local variable var. You can then use that to check the global variable.
To include a check that the variable is actually set:
proc x {varlist} {
foreach varname $varlist {
upvar #0 $varname var
if {[info exists var]} {
puts "$varname: [lindex {OFF ON} [string is true -strict $var]]"
} else {
puts "$varname: INDETERMINATE"
}
}
}

How to create Tcl Procedure with options?

I want to create tcl procedures with some options. I know procedures with arguments and optional arguments, but don't know options. For example, if I am calling my procedure arith in following three ways (-add for addition, -sub for subtraction):
1) arith 10 5
2) arith -add 10 5 or arith 10 5 -add
3) arith -sub 10 5 or arith 10 5 -sub
Respectively output should be 1) 15 (by default it should add), 2) 15, 3) 5
How to write this procedure in Tcl? I am new to tcl, please suggest me some online material or book for Tcl.
Complex argument parsing can be done with the cmdline package, which is part of Tcllib. The key command is ::cmdline::getoptions, which extracts the options from a variable and returns a dictionary describing them. It also modifies the variable so it contains just the arguments left over.
package require cmdline
proc arith args {
set options {
{op.arg "add" "operation to apply (defaults to 'add')"}
}
array set parsed [::cmdline::getoptions args $options]
if {[llength $args] != 2} {
return -code error "wrong # args: must be \"arith ?-op operation? x y\""
}
switch $parsed(op) {
add {return [::tcl::mathop::+ {*}$args]}
sub {return [::tcl::mathop::- {*}$args]}
default {
return -code error "Unknown -op \"$parsed(op)\": must be add or sub"
}
}
}
Demonstrating usage (including some error cases):
% arith
wrong # args: must be "arith ?-op operation? x y"
% arith 2 3
5
% arith -op sub 2 3
-1
% arith -op mult 2 3
Unknown -op "mult": must be add or sub
The main thing to be aware of is that the options descriptor takes the names of options without a leading - and with .arg on the end if you want to have an argument passed as well.
When it comes to options, it's a good idea to use even number of arguments
-op add -values {10 5}
-op sub -values {10 5}
With this, you can put the arguments into array as,
array set aArgs $args
where args is nothing but arguments passed to procedure.
proc arith {args} {
if {[catch {array set aArgs $args} err]} {
puts "Error : $err"
return 0
}
if {![info exists aArgs(-op)] || ![info exists aArgs(-values)] || [llength $aArgs(-values)]!=2} {
puts "Please pass valid args"
return 0
}
set x [lindex $aArgs(-values) 0]
set y [lindex $aArgs(-values) 1]
switch $aArgs(-op) {
"add" {
puts [expr {$x+$y}]
}
"sub" {
puts [expr {$x-$y}]
}
}
}
arith -op add -values {10 5}
In this example... I'm assuming that the operator is either the first or last option. The value would be -add or add. Just make the changes.
There is an optimization in the computation... to use lrange and ::mathop::.
proc arith args {
if {[llength $args] != 3} {
return -code error "wrong # args: must be \"arith ?-op operation? x y\""
}
set isadd [lsearch $args add]
set issub [lsearch $args sub]
if {$isadd != -1 && $issub == -1} {
return [expr [lindex $args 1] + [lindex $args [expr $isadd == 0 ? 2: 0]]]
}
if {$issub != -1 && $isadd == -1} {
return [expr [lindex $args [expr $issub == 0 ? 1: 0]] - [lindex $args [expr $issub == 0 ? 2: 1]]]
}
return -code error "Unknown -op must be add or sub"
}
example:
#arith add 1 2 3
#arith add sub 2
puts [arith add 1 2]
puts [arith 1 2 add]
puts [arith sub 1 2]
puts [arith 1 2 sub]
The two error examples were commented out because I need a catch instead... but it really depends on the big picture and how reusable it's intended to be.
proc arith args {
if {[llength $args] != 3} {
return -code error "wrong # args: must be \"arith ?-op operation? x y\""
}
set isadd [lsearch $args -add]
set issub [lsearch $args -sub]
if {$isadd != -1 && $issub == -1} {
return [::tcl::mathop::+ {*}[lrange $args [expr $isadd == 0] [expr ($isadd == 0) + 1]]]
}
if {$issub != -1 && $isadd == -1} {
return [::tcl::mathop::- {*}[lrange $args [expr $issub == 0] [expr ($issub == 0) + 1]]]
}
return -code error "Unknown -op must be add or sub"
}

sequential binary TCL

Please suggest improvements to my program. This program gives a 4 bit binary incremental o/p
I am looking for optimizing this where there is unnecessary code.
Please suggest improvements to my program. This program gives a 4 bit binary incremental o/p
I am looking for optimizing this where there is unnecessary code.
Please suggest improvements to my program. This program gives a 4 bit binary incremental o/p
I am looking for optimizing this where there is unnecessary code.
#!/bin/sh
# This
puts "+++++++++++++++++\n"
set ipaddr "0.0.0.0"
set limit 4
set splitip [split $ipaddr "."]
puts "Split ip address = $splitip"
# MAIN ROUTINE
set ilength [llength $splitip]
puts "Length of string is $ilength"
set first [lindex $splitip 0]
set sec [lindex $splitip 1]
set third [lindex $splitip 2]
set four [lindex $splitip 3]
for { set limit 1} { $limit >0} {} {
for { set first $first } { $first <= $limit} {} {
for { set sec $sec } { $sec <= $limit} {} {
for { set third $third } { $third <= $limit} {} {
for { set four $four } { $four <= $limit} {} {
puts " f:$first $sec $third $four"
incr four
}
set four 0
incr third; #puts " t:$four $third $sec $first\n"
}
set third 0
incr sec
}
#puts " f:$four $third $sec $first"
set sec 0
incr first
}
incr limit -1
}
# End Main
puts "\n++++++End Program+++++++++++"
Your program essentially boils down to this, does this do what you intended?
for { set first 0 } { $first <= 1} {incr first} {
for { set sec 0 } { $sec <= 1} {incr sec} {
for { set third 0 } { $third <= 1} {incr third} {
for { set four 0 } { $four <= 1} {incr four} {
puts " f:$first $sec $third $four"
}
}
}
}
Because if so, the primary suggestion is to simply remove everything except this.
Also: [llength $splitip] does not give you the string length of $splitip, but the list length. Those are different.
You're using a very roundabout way to assign values to first et al. Instead, use
lassign $splitip first sec third four
The lassign was added in Tcl 8.5. If you're using an older version of Tcl, use assignment by foreach instead:
foreach {first sec third four} $splitip break
The construct
for { set limit 1} { $limit >0} {incr limit -1} { ... }
simply means "execute ... exactly once": it doesn't affect the program's execution in any way. Even if you remove it (keeping the code inside the body argument), the code that was inside it will still execute exactly once.
For clarity, the incr x invocations should be inside the third argument to for, not inside the fourth, body, argument.
As a final note, if your intent is to print out a sequence of binary numbers, it's a lot easier to do that this way:
for {set i 0} {$i < 16} {incr i} { puts [format %04b $i] }

example code to make puts log the output

I'm looking for some Tcl code that would duplicate what puts command sends to stdout to some log file. Yes, there is a possibility to change all calls to puts to some custom function. But I would like to make it as transparent as possible.
I have this trial code, but it doesn't really work that well:
set pass_log_output "0"
rename puts _puts
proc puts { args } {
global pass_log_output
if {[info exists pass_log_output]} {
# There can be several cases:
# -nonewline parameter, stdout specified or not
set stdout_dest [ lsearch $args stdout ]
set nonewline [ lsearch $args -nonewline ]
if { $stdout_dest != -1 } {
log_low_level "" [lindex $args [expr $stdout_dest + 1]] ""
} elseif { $nonewline != -1 && [ llength $args ] > 1} {
log_low_level "" [lindex $args [expr $nonewline + 1]] ""
} else {
log_low_level "" [lindex $args 0] ""
}
}
if { [ catch { eval _puts $args } err ] } {
return -code error $err
}
}
log_low_level function just stores the passed string in a file.
So far I'm getting this error:
Tcl Interpreter Error: too many nested evaluations (infinite loop?)
Does log_low_level use puts? That could be your infinite loop.
If so, try changing it to use _puts.
Thanks for the points. I just want to post the final working code for reference. It even takes care of the storing lines with -nonewline flag properly.
set pass_log_output "0"
set last_call_nonewline 0
rename puts _orig_puts
proc puts { args } {
global pass_log_output
global g_log_file
global last_call_nonewline
if {[info exists pass_log_output]} {
# Check if the logging was initialized
if {![info exists g_log_file]} {
_orig_puts "Log file wasn't initialized!"
return
}
# There can be several cases:
# -nonewline parameter, stdout specified or not
set stdout_dest [ lsearch $args stdout ]
set nonewline [ lsearch $args -nonewline ]
if {[ llength $args ] > 3} {
return -code error "wrong # args: should be puts ?-nonewline? ?channelId? string"
} elseif { $stdout_dest != -1 } {
set message [lindex $args end]
} elseif { $nonewline != -1 && [ llength $args ] == 2} {
set message [lindex $args [expr $nonewline + 1]]
} elseif {[ llength $args ] == 1} {
set message [lindex $args 0]
}
# Store the message in the file, if needed.
# Take into account if the last call was with -nonewline
if {[info exists message]} {
if {$last_call_nonewline == 0} {
_orig_puts -nonewline $g_log_file [clock format [clock seconds] -format "%T - "]
}
if {$nonewline != -1} {
set last_call_nonewline 1
_orig_puts -nonewline $g_log_file "$message"
} else {
set last_call_nonewline 0
_orig_puts $g_log_file "$message"
}
flush $g_log_file
}
}
if { [ catch { eval _orig_puts $args } err ] } {
return -code error $err
}
}
Since puts has very few options, it may be easier to consider the number of args given. Also, you should contain all uses of the original _puts to your new puts proc -- this new puts should be transparent even to your code.
I assume you only want to log stuff you're writing to stdout
rename puts _orig_puts
proc puts {args} {
switch -exact [llength $args] {
3 {
# both -newline and a channelId are given
set do_log [expr {[lindex $args 1] eq "stdout"}]
}
2 {
# only log if not writing to stdout
set chan [lindex $args 0]
set do_log [expr {$chan eq "-nonewline" || $chan eq "stdout"}]
}
1 {
set do_log true
}
default {
error {wrong # args: should be "puts ?-nonewline? ?channelId? string"}
}
}
if {$do_log} {
set chan [open $::mylogfile a]
_orig_puts $chan [lindex $args end]
close $chan
}
_orig_puts {*}$args
}