get key value from tcl dict with a default value - tcl

Very new/rusty with TCL here :-(. I am stuck with tcl 8.6 and can't take advantage of tcl 8.7 feature of getwithdefault on a dict.
Tried the following and get error saying "frameLen" is not part of the dict. But I thought the ternary operator should have skiped the part on [dict get $pktBlock frameLen]. What did I do wrong?
Thanks!
package require json
set pktBlock [json::json2dict {{"frameLenn": 253}}]
set frameLen [expr [dict exists $pktBlock framelen] ? [dict get $pktBlock frameLen]: 256 ]

My version (Different syntax than what's slated for 8.7; the default value is optional here and defaults to an empty string):
# dict getdef dictValue -default arg path...
if {[::info commands ::tcl::dict::getdef] eq {}} {
proc ::tcl::dict::getdef {dictionary args} {
::set def {}
if {[lindex $args 0] eq "-default"} {
::set args [::lassign $args _ def]
}
if {[exists $dictionary {*}$args]} {
get $dictionary {*}$args
} else {
::set def
}
}
namespace ensemble configure \
dict -map [dict replace [namespace ensemble configure dict -map] \
getdef ::tcl::dict::getdef]
}

Turned out I had a wrong spelling. I intended to have dict exists $pktBlock frameLen but ended up using framelen.
Case is VERY important. Hope this could be a lesson for those who are stuck

It's not too difficult to implement this feature in plain Tcl:
# dict getdef dictionaryValue ?key ...? key default
#
# This will be in Tcl 8.7 -- https://tcl.tk/man/tcl8.7/TclCmd/dict.htm
proc dict_getdef {dictValue args} {
if {[llength $args] < 2} {
error {wrong # args: should be "dict getdef dictValue ?key ...? key default"}
}
set default [lindex $args end]
set keys [lrange $args 0 end-1]
if {[dict exists $dictValue {*}$keys]} {
return [dict get $dictValue {*}$keys]
} else {
return $default
}
}
This can be added as a dict subcommand like this:
set map [namespace ensemble configure dict -map]
dict set map getdef [namespace which dict_getdef]
namespace ensemble configure dict -map $map
So that:
set frameLen [dict getdef $pktBlock frameLen 256]

Related

What is the proper way to create hyphen parameters in TCL that are Boolean or have values? [duplicate]

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.

Problem creating new command in `array` ensemble

I want to create a convenience command array values arrayName as a "flip side" to the "array names" command.
It's straightforward to create a simple proc:
proc array_values {arrayName} {
upvar 1 $arrayName ary
set values {}
foreach {name value} [array get ary] {lappend values $value}
return $values
}
array set a {foo bar baz qux}
puts [array_values a] ;# => bar qux
However, I'm having difficulty creating a command in the ::tcl::array namespace:
first some homework:
is array a namespace ensemble? Yes.
% namespace ensemble exists array
1
what is the namespace?
% namespace ensemble configure array -namespace
::tcl::array
what are the subcommands?
% namespace ensemble configure array -subcommands
% namespace ensemble configure array -map
anymore ::tcl::array::anymore donesearch ::tcl::array::donesearch exists ::tcl::array::exists get ::tcl::array::get names ::tcl::array::names nextelement ::tcl::array::nextelement set ::tcl::array::set size ::tcl::array::size startsearch ::tcl::array::startsearch statistics ::tcl::array::statistics unset ::tcl::array::unset
OK, all good so var. Let's add that array_values proc into the namespace
% namespace eval ::tcl::array {
proc values {arrayName} {
upvar 1 $arrayName ary
set values {}
foreach {name value} [array get ary] {lappend values $value}
return $values
}
}
% array set a {foo bar baz qux}
% puts [::tcl::array::values a]
can't set "values": variable is array
Where is this error coming from? I tried renaming the "values" variable in the proc to other names, but it still emits the "variable is array" error.
a note: I can add the first proc to the ensemble:
% namespace ensemble config array -map [list values ::array_values {*}[namespace ensemble config array -map]]
% array values a
bar qux
But what is wrong with my ::tcl::array::values proc?
Your set values {} command executes in the ::tcl::array namespace, so it runs the ::tcl::array::set command. In other words, it does the equivalent of array set values {}. So it makes values an array with no members. Then the lappend values $value command fails because values is an array at that point.
The solution should be to use ::set values {}
Or you can completely avoid the issue by using:
proc array_values {arrayName} {
upvar 1 $arrayName ary
return [lmap {name value} [get ary] {string cat $value}]
}
I would like to add that, given that the presence of possibly conflicting ensemble commands is a moving target, patching an ensemble is likely to occur from everywhere, I have seen core developers keep extra ensemble commands outside the ::tcl::array::* namespace:
proc arrayValues {arrayName} {
upvar 1 $arrayName ary
set values {}
foreach {name value} [array get ary] {lappend values $value}
return $values
}
# implant "arrayValues" into [array] ensemble as "values"
namespace ensemble configure ::array -map \
[dict replace [namespace ensemble configure ::array -map] \
values [namespace which arrayValues]]
This way, you don't have to worry about unintended resolution conflicts (whatever that means in Tcl, to begin with).
For the curious, this is what I have ended up with:
$HOME/tcl/lib/monkeypatches/monkeypatches.tcl
# a set of useful additions to built-in ensembles
package provide monkeypatches 0.1
namespace eval ::monkeypatches {
# https://wiki.tcl-lang.org/page/wrapping+commands
proc append_subcommand {cmd subcmd procname} {
set map [namespace ensemble configure $cmd -map]
dict set map $subcmd [namespace which $procname]
namespace ensemble configure $cmd -map $map
}
# array foreach
# to be subsumed by https://core.tcl.tk/tips/doc/trunk/tip/421.md
#
# example:
# array set A {foo bar baz qux}
# array foreach {key val} A {puts "name=$key, value=$val"}
#
proc array_foreach {vars arrayName body} {
if {[llength $vars] != 2} {
error {array foreach: "vars" must be a 2 element list}
}
lassign $vars keyVar valueVar
# Using the complicated `upvar 1 $arrayName $arrayName` so that any
# error messages propagate up with the user's array name
upvar 1 $arrayName $arrayName \
$keyVar key \
$valueVar value
set sid [array startsearch $arrayName]
# If the array is modified while a search is ongoing, the searchID will
# be invalidated: wrap the commands that use $sid in a try block.
try {
while {[array anymore $arrayName $sid]} {
set key [array nextelement $arrayName $sid]
set value [set "${arrayName}($key)"]
uplevel 1 $body
}
array donesearch $arrayName $sid
} trap {TCL LOOKUP ARRAYSEARCH} {"" e} {
return -options $e "detected attempt to modify the array while iterating"
}
return
}
append_subcommand ::array foreach array_foreach
# array values arrayName
# https://stackoverflow.com/q/53379995/7552
#
# example:
# array set x {foo bar baz qux}
# array get x ;# => foo bar baz qux
# array names x ;# => foo baz
# array values x ;# => bar qux
#
proc array_values {arrayName} {
upvar 1 $arrayName ary
set values [list]
array foreach {name value} ary {lappend values $value}
return $values
}
append_subcommand ::array values array_values
# info formalargs procName
# https://core.tcl.tk/tips/doc/trunk/tip/65.md
#
# example:
# proc test {one {two 2} {three {3 4 5}} args} {return}
# info args test ;# => one two three args
# info formalargs test ;# => one {two 2} {three {3 4 5}} args
#
proc info_formalargs {procname} {
# [info args] throws an error if $procname is not a procedure.
return [lmap arg [info args $procname] {
set has_d [info default $procname $arg value]
if {$has_d} then {list $arg $value} else {set arg}
}]
}
append_subcommand ::info formalargs info_formalargs
}
With its associated pkgIndex.tcl
And $HOME/.tclshrc
set lib_dir [file join $env(HOME) tcl lib]
if {$lib_dir ni $auto_path} {lappend auto_path $lib_dir}
unset lib_dir
package require monkeypatches

Redirecting the "parray" output to a file in tcl

I have an array in tcl.
For example:
set a(1) "First element"
set a(2) "second element"
parray a
parray a displays output as
a(1) = "First element"
a(2) = "second element"
Is it possible to redirect the parray output to a file?
The parray command can't be redirected. It's a simple-minded procedure that is too stupid to be redirected. But it's source code isn't very long; in fact, it's short enough that I'll just paste it here (it's under the Tcl license):
proc parray {a {pattern *}} {
upvar 1 $a array
if {![array exists array]} {
return -code error "\"$a\" isn't an array"
}
set maxl 0
set names [lsort [array names array $pattern]]
foreach name $names {
if {[string length $name] > $maxl} {
set maxl [string length $name]
}
}
set maxl [expr {$maxl + [string length $a] + 2}]
foreach name $names {
set nameString [format %s(%s) $a $name]
puts stdout [format "%-*s = %s" $maxl $nameString $array($name)]
}
}
Redirecting it (hint: change the stdout for something obtained from open … a, and don't forget to close it afterwards) should be a simple exercise.
This builds on the answers by Dinesh and Donal Fellows: You could adapt the code of parray automatically, like this:
auto_load parray
proc printArray {a {pattern *} {channel stdout}} \
[string map {stdout $channel} [info body parray]]
This gives you a new proc printArray with an optional channel argument.

how to create tcl proc with hyphen flag arguments

Im searching all over the internet , i guess im searching not the right keywords
i tried most of them :)
i want to create in tcl/bash a proc with hyphen flags to get arguments with flags from the user
ex.
proc_name -color red -somethingselse black
It's very easy to do, actually. This code allows abbreviated option names, flag options (-quxwoo in the example) and the ability to stop reading options either with a -- token or with a non-option argument appearing. In the example, unknown option names raise errors. After passing the option-parsing loop, args contains the remaining command-line arguments (not including the -- token if it was used).
proc foo args {
array set options {-bargle {} -bazout vampires -quxwoo 0}
while {[llength $args]} {
switch -glob -- [lindex $args 0] {
-bar* {set args [lassign $args - options(-bargle)]}
-baz* {set args [lassign $args - options(-bazout)]}
-qux* {set options(-quxwoo) 1 ; set args [lrange $args 1 end]}
-- {set args [lrange $args 1 end] ; break}
-* {error "unknown option [lindex $args 0]"}
default break
}
}
puts "options: [array get options]"
puts "other args: $args"
}
foo -barg 94 -quxwoo -- abc def
# => options: -quxwoo 1 -bazout vampires -bargle 94
# => other args: abc def
This is how it works. First set default values for the options:
array set options {-bargle {} -bazout vampires -quxwoo 0}
Then enter a loop that processes the arguments, if there are any (left).
while {[llength $args]} {
During each iteration, look at the first element in the argument list:
switch -glob -- [lindex $args 0] {
String-match ("glob") matching is used to make it possible to have abbreviated option names.
If a value option is found, use lassign to copy the value to the corresponding member of the options array and to remove the first two elements in the argument list.
-bar* {set args [lassign $args - options(-bargle)]}
If a flag option is found, set the corresponding member of the options array to 1 and remove the first element in the argument list.
-qux* {set options(-quxwoo) 1 ; set args [lrange $args 1 end]}
If the special -- token is found, remove it from the argument list and exit the option-processing loop.
-- {set args [lrange $args 1 end] ; break}
If an option name is found that hasn't already been dealt with, raise an error.
-* {error "unknown option [lindex $args 0]"}
If the first argument doesn't match any of the above, we seem to have run out of option arguments: just exit the loop.
default break
Documentation: array, break, error, lassign, lindex, llength, proc, puts, set, switch, while
With array set, we can assign the parameters and their values into an array.
proc getInfo {args} {
# Assigning key-value pair into array
# If odd number of arguments passed, then it should throw error
if {[catch {array set aInfo $args} msg]} {
return $msg
}
parray aInfo; # Just printing for your info
}
puts [getInfo -name Dinesh -age 25 -id 974155]
will produce the following output
aInfo(-age) = 25
aInfo(-id) = 974155
aInfo(-name) = Dinesh
The usual way to handle this in Tcl is by slurping the values into an array or dictionary and then picking them out of that. It doesn't offer the greatest amount of error checking, but it's so easy to get working.
proc myExample args {
# Set the defaults
array set options {-foo 0 -bar "xyz"}
# Read in the arguments
array set options $args
# Use them
puts "the foo option is $options(-foo) and the bar option is $options(-bar)"
}
myExample -bar abc -foo [expr {1+2+3}]
# the foo option is 6 and the bar option is abc
Doing error checking takes more effort. Here's a simple version
proc myExample args {
array set options {-foo 0 -bar "xyz"}
if {[llength $args] & 1} {
return -code error "must have even number of arguments in opt/val pairs"
}
foreach {opt val} $args {
if {![info exist options($opt)]} {
return -code error "unknown option \"$opt\""
}
set options($opt) $val
}
# As before...
puts "the foo option is $options(-foo) and the bar option is $options(-bar)"
}
myExample -bar abc -foo [expr {1+2+3}]
# the foo option is 6 and the bar option is abc
# And here are the errors it spits out...
myExample -spregr sgkjfd
# unknown option "-spregr"
myExample -foo
# must have even number of arguments in opt/val pairs
#flag defaults
set level 1
set inst ""
# Parse Flags
while {[llength $args]} {
set flag [lindex $args 0]
#puts "flag: ($flag)"
switch -glob $flag {
-level {
set level [lindex $args 1]
set args [lrange $args 2 end]
puts "level:($level) args($args)"
} -inst {
set autoname 0
set inst [lindex $args 1]
set args [lrange $args 2 end]
puts "inst:($inst) args($args)"
} -h* {
#help
puts "USAGE:"
exit 1
} -* {
# unknown option
error "unknown option [lindex $args 0]"
} default break
}
}
# remaining arguments
set filename "$args"
puts "filename: $args"

How can I safely deal with optional parameters

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.