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"
}
}
}
Related
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.
I am writing a proc to create a header in an output file.
Currently it needs to take an optional parameter, which is a possible comment for the header.
I have ended up coding this as a single optional parameter
proc dump_header { test description {comment = ""}}
but would like to know how I can achieve the same using args
proc dump_header { test description args }
It's quite easy to check for args being a single blank parameter ($args == ""), but doesn't cope well if passing multiple parameters - and I need the negative check anyway.
Your proc definition is incorrect (you'd get the error message too many fields in argument specifier "comment = """). Should be:
proc dump_header { test description {comment ""}} {
puts $comment
}
If you want to use args, you could examine the llength of it:
proc dump_header {test desc args} {
switch -exact [llength $args] {
0 {puts "no comment"}
1 {puts "the comment is: $args"}
default {
puts "the comment is: [lindex $args 0]"
puts "the other args are: [lrange $args 1 end]"
}
}
}
You might also want to pass name-value pairs in a list:
proc dump_header {test desc options} {
# following will error if $options is an odd-length list
array set opts $options
if {[info exists opts(comment)]} {
puts "the comment is: $opts(comment)"
}
puts "here are all the options given:"
parray opts
}
dump_header "test" "description" {comment "a comment" arg1 foo arg2 bar}
Some prefer a combination of args and name-value pairs (a la Tk)
proc dump_header {test desc args} {
# following will error if $args is an odd-length list
array set opts $args
if {[info exists opts(-comment)]} {
puts "the comment is: $opts(-comment)"
}
parray opts
}
dump_header "test" "description" -comment "a comment" -arg1 foo -arg2 bar
I use tcllib's cmdline library to do option parsing.
This is the example from cmdline documentation:
set options {
{a "set the atime only"}
{m "set the mtime only"}
{c "do not create non-existent files"}
{r.arg "" "use time from ref_file"}
{t.arg -1 "use specified time"}
}
set usage ": MyCommandName \[options] filename ...\noptions:"
array set params [::cmdline::getoptions argv $options $usage]
if { $params(a) } { set set_atime "true" }
set has_t [expr {$params(t) != -1}]
set has_r [expr {[string length $params(r)] > 0}]
if {$has_t && $has_r} {
return -code error "Cannot specify both -r and -t"
} elseif {$has_t} {
...
}
So, in your case, you'd just use args in place of argv in the above example.
It should be mentioned explicitly that args is a special word in Tcl that, when used at the end of the argument list, contains a list of all the remaining arguments. If no args are given, then no error is produced (unlike any other variable name, which would be considered a required argument).
I was looking for a way to have functionality similar to python's kwargs (optional key-value pair arguments), and something that works nicely is (similar to Glenn's last example):
proc my_proc {positional_required1 {positional_optional1 "a_string"} args} {
# Two optional arguments can be given: "opt1" and "opt2"
if {![string equal $args ""]} {
# If one or more args is given, parse them or assign defaults.
array set opts $args
if {[info exists opts(opt1)]} { set opt1 $opts(opt1) } else { set opt1 0 }
if {[info exists opts(op2)]} { set opt2 $opts(opt2) } else { set opt2 -1 }
} else {
# If no args are given, assign default values.
set op1 0
set op2 -1
}
# DO STUFF HERE
}
And can be called like:
my_proc "positional_required1_argument"
# OR
my_proc "positional_required1_argument" "a_string"
# OR
my_proc "positional_required1_argument" "a_string" opt1 7
# OR
my_proc "positional_required1_argument" "a_string" opt1 7 opt2 50
# etc.
A potential downside (as I've currently implemented it) is that if a user passes a non-approved key-value option, there is no error.
I want to generate several comboboxs and buttons in the for loop, and the button command will call the function and check the combobox content, how to get the variable "com$num" and pass to the "get_optimizer" function? How to correct below script? Pls help, thanks!
set num 1
foreach SQ {1 2 3 4 5} {
ttk::combobox $twind.frame.dpcom$num -textvariable com$num -values {Global Definitive Adaptive Cmaes}
button $twind.frame.but$num -text "Optimizer Setting" -command [list get_optimizer]
grid $twind.frame.dpcom$num -row $num -column 0
grid $twind.frame.but$num -row $num -column 1
incr num}
proc get_optimizer {} {
global [set com$num]
if {[set com$num]=='Global'} {
...
} elseif {[set com$num]=='Definitive'} {
...
} elseif {...} {
...}
...
}
You should pass the whole name of the variable into get_optimizer, and use upvar #0 to give that a fixed local alias name inside the procedure.
# backslash-newline for readability only
button $twind.frame.but$num -text "Optimizer Setting" \
-command [list get_optimizer com$num]
proc get_optimizer {varname} {
upvar #0 $varname theVar
if {$theVar=='Global'} {
...
} elseif {$theVar=='Definitive'} {
...
} elseif {...} {
...
}
...
}
Also, it's more efficient to use the eq operator for string equality. And consider whether it would be better to use an array (i.e., com(1) instead of com1).
Use
global com$num
(giving you, say, global com1)
instead of
global [set com$num]
(giving you, say, global Definitive)
I made a simple program accepting short user inputs and storing them in a file. With another button displaying each input in the file as a button. I am trying to create another proc that deletes the button and also the string in the file when i click on the button generated by the string. How do i do that? i tried regsub on the variable holding the values but it seems to delete them once and not every time.
Code to grab current directory
catch { set abspath [file readlink [info script]]}
if { ![info exists abspath]} { set abspath $argv0 }
if { [regexp {^(\S+)\/\S+} $abspath matched dir]} { set BIN $dir }
file mkdir $BIN/debug
if {[file exists $BIN/debug/debug.txt]} { close [open $BIN/debug/debug.txt "w"]}
GUI Code
label .lbl -text "Enter something"
entry .en -justify center
button .sub -text "SUBMIT" -command "submit .en"
button .sho -text "SHOW" -command sho
button .cl -text "CLEAR" -command clear
grid .lbl -columnspan 3
grid .en -columnspan 3
grid .sub .sho .cl
Submit Procedure
proc submit {ent} {
global BIN
if {![file isdirectory $BIN/debug]} { file mkdir $BIN/debug }
set input [$ent get]
if {$input == "" || [string is space -strict $input]} {
$ent delete 0 end
.lbl configure -text "No empty strings"
} else {
set fp [open $BIN/debug/debug.txt a+]
$ent delete 0 end
puts $fp $input
close $fp
}
}
Clear Procedure
proc clear {} {
global BIN
if {[file exists $BIN/debug/debug.txt]} { close[open $BIN/debug/debug.txt "w"] }
}
Procedure to generate button for each item in file
proc sho {} {
global BIN
global filedat
set w.gui
if {[info exists filedat]} { set filedat "" }
toplevel $w
wm title "values"
wm overrideredirect $w 1
bind $w <Button-3> "destroy $w"
if {[file exists $BIN/debug/debug.txt]} {
set fp [open $BIN/debug/debug.txt r]
while {[gets $fp data] > -1} {
lappend filedat $data
}
close $fp
if {[info exist filedat]} {
set dcount 0
foreach item $filedat {
button $w.bn$dcount -text "$item" -font [list arial 10] -anchor w -fg white -bg black -command "del $item"
grid $w.bn$dcount -sticky w
incr dcount
}
} else {
label $w.nthLabel -text "Nothing in file" -bg black -fg white
grid $w.nthLabel
}
}
}
Procedure to delete string (currently not working as expected)
proc del {st} {
global filedat
regsub -all $st $filedat "" filedat2
puts $filedat2
}
When you delete the string with your dep proc you saved the new string in the variable filedat2.
The global variable filedat is never changed.
If you want to remove the string from your global variable, you have to pass this variable to regsub, instead of filedat2.
regsub -all $st $filedat "" filedat
Or if you prefer to save it in a temporal variable to perform some test you could use filedat2 and then assign the variable again:
regsub -all $st $filedat "" filedat2
# ... the tests
if {[isOk]} {
# update the variable
set filedat $filedat2
} else {
# leave the previous value
puts "some error here"
}
I am writing a proc to create a header in an output file.
Currently it needs to take an optional parameter, which is a possible comment for the header.
I have ended up coding this as a single optional parameter
proc dump_header { test description {comment = ""}}
but would like to know how I can achieve the same using args
proc dump_header { test description args }
It's quite easy to check for args being a single blank parameter ($args == ""), but doesn't cope well if passing multiple parameters - and I need the negative check anyway.
Your proc definition is incorrect (you'd get the error message too many fields in argument specifier "comment = """). Should be:
proc dump_header { test description {comment ""}} {
puts $comment
}
If you want to use args, you could examine the llength of it:
proc dump_header {test desc args} {
switch -exact [llength $args] {
0 {puts "no comment"}
1 {puts "the comment is: $args"}
default {
puts "the comment is: [lindex $args 0]"
puts "the other args are: [lrange $args 1 end]"
}
}
}
You might also want to pass name-value pairs in a list:
proc dump_header {test desc options} {
# following will error if $options is an odd-length list
array set opts $options
if {[info exists opts(comment)]} {
puts "the comment is: $opts(comment)"
}
puts "here are all the options given:"
parray opts
}
dump_header "test" "description" {comment "a comment" arg1 foo arg2 bar}
Some prefer a combination of args and name-value pairs (a la Tk)
proc dump_header {test desc args} {
# following will error if $args is an odd-length list
array set opts $args
if {[info exists opts(-comment)]} {
puts "the comment is: $opts(-comment)"
}
parray opts
}
dump_header "test" "description" -comment "a comment" -arg1 foo -arg2 bar
I use tcllib's cmdline library to do option parsing.
This is the example from cmdline documentation:
set options {
{a "set the atime only"}
{m "set the mtime only"}
{c "do not create non-existent files"}
{r.arg "" "use time from ref_file"}
{t.arg -1 "use specified time"}
}
set usage ": MyCommandName \[options] filename ...\noptions:"
array set params [::cmdline::getoptions argv $options $usage]
if { $params(a) } { set set_atime "true" }
set has_t [expr {$params(t) != -1}]
set has_r [expr {[string length $params(r)] > 0}]
if {$has_t && $has_r} {
return -code error "Cannot specify both -r and -t"
} elseif {$has_t} {
...
}
So, in your case, you'd just use args in place of argv in the above example.
It should be mentioned explicitly that args is a special word in Tcl that, when used at the end of the argument list, contains a list of all the remaining arguments. If no args are given, then no error is produced (unlike any other variable name, which would be considered a required argument).
I was looking for a way to have functionality similar to python's kwargs (optional key-value pair arguments), and something that works nicely is (similar to Glenn's last example):
proc my_proc {positional_required1 {positional_optional1 "a_string"} args} {
# Two optional arguments can be given: "opt1" and "opt2"
if {![string equal $args ""]} {
# If one or more args is given, parse them or assign defaults.
array set opts $args
if {[info exists opts(opt1)]} { set opt1 $opts(opt1) } else { set opt1 0 }
if {[info exists opts(op2)]} { set opt2 $opts(opt2) } else { set opt2 -1 }
} else {
# If no args are given, assign default values.
set op1 0
set op2 -1
}
# DO STUFF HERE
}
And can be called like:
my_proc "positional_required1_argument"
# OR
my_proc "positional_required1_argument" "a_string"
# OR
my_proc "positional_required1_argument" "a_string" opt1 7
# OR
my_proc "positional_required1_argument" "a_string" opt1 7 opt2 50
# etc.
A potential downside (as I've currently implemented it) is that if a user passes a non-approved key-value option, there is no error.