tcl set list of arrays produce duplicates - tcl

I'm producing a TCL procedure that will return a list of arrays of devices under a switch. The definition is an XML file that is read. The resulting lists of XML entries are parsed using a recursive procedure and the device attributes are placed in an array.
Each array is then placed in a list and reflected back to the caller. My problem is that when I print out the list of devices, the last device added to the list is printed out each time. The contents of the list is all duplicates.
Note: I'm using the excellent proc, 'xml2list' that was found here. I'm sorry, I forgot who submitted this.
The following code illustrates the problem:
source C:/src/tcl/xml2list.tcl
# Read and parse XML file
set fh [open C:/data/tcl/testfile.xml r]
set myxml [read $fh]
set mylist [xml2list $myxml]
array set mydevice {}
proc devicesByName { name thelist list_to_fill} {
global mydevice
global set found_sw 0
upvar $list_to_fill device_arr
foreach switch [lindex $thelist 2] {
set atts [lindex $switch 1]
if { [lindex $switch 0] == "Switch" } {
if { $name == [lindex $atts 3] } {
set found_sw 1
puts "==== Found Switch: $name ===="
} else {
set found_sw 0
}
} elseif { $found_sw == 1 && [string length [lindex $atts 3]] > 0 } {
set mydevice(hdr) [lindex $switch 0]
set mydevice(port) [lindex $atts 1]
set mydevice(name) [lindex $atts 3]
set mydevice(type) [lindex $atts 5]
puts "Device Found: $mydevice(name)"
set text [lindex $switch 2]
set mydevice(ip) [lindex [lindex $text 0] 1]
lappend device_arr mydevice
}
devicesByName $name $switch device_arr
}
}
#--- Call proc here
# set a local array var and send to the proc
set device_arr {}
devicesByName "Switch1" $mylist device_arr
# read out the contents of the list of arrays
for {set i 0} {$i<[llength $device_arr]} {incr i} {
upvar #0 [lindex $device_arr $i] temp
if {[array exists temp]} {
puts "\[$i\] Device: $temp(name)-$temp(ip)"
}
}
The XML file is here:
<Topology>
<Switch ports="48" name="Switch1" ip="10.1.1.3">
<Device port="1" name="RHEL53-Complete1" type="host">10.1.1.10</Device>
<Device port="2" name="Windows-Complete1" type="host">10.1.2.11</Device>
<Device port="3" name="Solaris-Complete1" type="host">10.1.2.12</Device>
</Switch>
<Switch ports="36" name="Switch2" ip="10.1.1.4">
<Device port="1" name="Windows-Complete2" type="host">10.1.3.10</Device>
</Switch>
<Router ports="24" name="Router1" ip="10.1.1.2">
<Device port="1" name="Switch1" type="switch">10.1.1.3</Device>
<Device port="2" name="Switch2" type="switch">10.1.1.4</Device>
</Router>
</Topology>
If my code blocks look bad, please excuse that. I followed the directions as I read them, but it didn't look correct. I could not fix it, so just posted anyway.
Thanks in advance...

Arrays in tcl are not values. Therefore they don't behave like regular variables. They are in fact something special like filehandles or sockets.
You cannot assign an array to a list like that. Doing:
lappend device_arr mydevice
simply appends the string "mydevice" to the list device_arr. That string happens to be the name of a global variable so that string may be used later to access that global variable.
To build up a key-value data structure what you want is a dict. You can think of a dict as a special list that has even numbers of elements in the format: {key value key value}. In fact, this data structure works even on very old versions of tcl before the introduction of dicts because the foreach loop in tcl can be used to process key-value pairs.
So what you want is to create a new $mydevice dict each loop and use [dict set] to assign the values.
Alternatively you can keep most of your code and change your lappend to:
lappend device_arr [array get mydevice]
This works because [array get] returns a key-value list which can be treated as a dict. You can later access the data using the dict command.

Array variables can't be used as values. To put the contents of one into a list element, send it to a proc, write it to a file etc, convert it to list form (key, value, key, value...) with array get.
lappend device_arr [array get mydevice]
To use it later, write the list back to an array with array set.
foreach device_l $device_arr {
#array unset device
array set device $device_l
puts "$device(name)-$device(ip)"
}
Note that array set doesn't erase the old keys in the destination array, so if you use it in a loop and the key names aren't always the same, you need to clear the array every iteration.

You can store this information in two ways using arrays . First is as a multi-dimensional array, in this case a three dimensional array and the second is a one dimensional array storing a list that can be converted easily to an array later for accessing data at a later time.
For the 3d array the key would be Switch Name,device_port,dataname you would change your erroneous temporary myDevice and lappend code to
# attr is a list of { attributename1 value1 ... attributenameN valueN}
array set temp $attr
set port $temp(port)
set text [lindex $switch 2]
set ip [lindex [lindex $text 0] 1]
# name already set to "Switch1" etc
foreach f [array names temp ] {
set device_arr($name,$port,$f) $temp($f)
}
set device_arr($name,$port,ip) $ip
array unset temp
this code results in the following ( when parray device_arr
parray device_arr
device_arr(Switch1,1,name) "Switch1"
device_arr(Switch1,1,port) 1
device_arr(Switch1,1,type) "RedHat .."
device_arr(Switch1,1,ip) 10..
device_arr(Switch1,2,name) "Switch1"
device_arr(Switch1,2,port) 1
device_arr(Switch1,2,type) "RedHat .."
device_arr(Switch1,2,ip) 10..
...
device_arr(Switch2,1,name) "Switch1"
device_arr(Switch2,1,port) 1
device_arr(Switch2,1,type) "Windows Complete"
device_arr(Switch2,1,ip) 10..
....
to find ip of Switch1 port2 you would:
puts "the ip of Switch1 port 2 is $device_arr(Switch1,2,ip)"
Note lots of data duplication but you can access all data directly without having to go to an intermediate step to get to the data as in the next scheme
# attr is a list of { attributename1 value1 ... attributenameN valueN}
set data $attr
array set temp $attr
set text [lindex $switch 2]
set ip [lindex [lindex $text 0] 1]
lappend data ip $ip
set key "$name,$temp(port)"
# name already set to "Switch1" etc
set device_arr($name,$port) $data
array unset temp
doing a parray device_arr gives:
device_arr(Switch1,1) { port "1" name "RHEL53-Complete1" type "host" ip 10.1.1.10 }
device_arr(Switch1,2) { port "2" name "Windows-Complete1" type "host" ip 10.1.2.11}
....
to find the ip of swtich1 port 2 you would
array set temp $device_array(Switch1,2)
puts "ip of device 2 is $temp(ip)"

Related

sorting a tcl dictionary inside txt file

I need some help with writing a tcl code, to sort the data from a dictionary. The dictionary saves lists in a .txt file. I need to access the file and sort it through the second column of the lists.
1,0.8,bananas,,,,,
2,1.0,apples,,,,,
3,5.1,grapes,,,,,
4,2.4,oranges,,,,,
5,1.7,pineapples,,,,,
...
how can i sort that dict data, to look like that:
1,0.8,bananas,,,,,
2,1.0,apples,,,,,
5,1.7,pineapples,,,,,
4,2.4,oranges,,,,,
3,5.1,grapes,,,,,
...
please, can you help me making this sorting code?
i tried many codes, but with no sucess.
Tcl's lsort -command option would be useful here.
To use it, first define a proc that takes two arguments (usually called a and b) which returns -1, 0, or 1. Each pair of items in the list will be used as a pair of arguments to this proc.
set lines {
1,0.8,bananas,,,,,
2,1.0,apples,,,,,
3,5.1,grapes,,,,,
4,2.4,oranges,,,,,
5,1.7,pineapples,,,,,
}
proc sort_by_col2 {a b} {
set list_a [split $a ","]
set list_b [split $b ","]
set a2 [lindex $list_a 1]
set b2 [lindex $list_b 1]
if {$a2 < $b2} {
return -1
} elseif {$a2 > $b2} {
return 1
} else {
return 0
}
}
lsort -command sort_by_col2 $lines
--> 1,0.8,bananas,,,,,
2,1.0,apples,,,,,
5,1.7,pineapples,,,,,
4,2.4,oranges,,,,,
3,5.1,grapes,,,,,

Getting the next key in an array

My goal is to find the next key in an array... below my data :
# Index increment may change, there is not necessarily continuity like this example.
# My $index can be 1,2,3,4,8,12,25,32...
# but the size of my array is about 100,000 elements.
for {set index 1} {$index < 100000} {incr index} {
set refdata($index,Pt,X) [expr {10 + $index}]
}
I need to know the next key to be able to build a geometric line... I did not find in the help a command that allows me to find the next key of my array so I created my own function below :
proc SearchNextKeyArrayElement {dataarray mykey} {
upvar $dataarray myarray
set mydata [lsort -dictionary [array names myarray]]
set index [lsearch $mydata $mykey]
if {$index > -1} {
return [lindex $mydata [expr {$index + 1}]]
}
return ""
}
foreach k [lsort -dictionary [array names refdata]] {
if {[string match "*,Pt,*" $k]} {
set nextkey [SearchNextKeyArrayElement refdata $k]
}
}
And it takes a long time...array nextelement command is maybe the solution...But I do not understand how to use it ?
Here's an example:
start a search with array startsearch
loop while array anymore is true
get the next key with array nextelement
tidy up with array donesearch
use try {} catch {} finally for safety
# array foreach
# to be subsumed in Tcl 8.7 by `array for`
# 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"}
#
# A note on performance: we're not saving any time with this approach.
# This is essentially `foreach name [array names ary] {...}
# We are saving memory: iterating over the names versus extracting
# them all at the beginning.
#
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
}
} trap {TCL LOOKUP ARRAYSEARCH} {"" e} {
puts stderr [list $e]
dict set e -errorinfo "detected attempt to add/delete array keys while iterating"
return -options $e
} finally {
array donesearch $arrayName $sid
}
return
}
Generally speaking, Tcl arrays have no order at all; they can change their order on any modification to the array or any of its elements. The commands that iterate over the array (array for, array get, array names, and the iteration commands array startsearch/array nextelement/array anymore) only work with the current order. However, you can use array names to get the element names into a Tcl list (which is order preserving), sort those to get the order that you're going to iterate over, and then use foreach over that. As long as you're not adding or removing elements, it'll be fine. (Adding elements is sort-of OK too; you'll just not see them in your iteration.)
foreach key [lsort -dictionary [array names myarray]] {
ProcessElement $key $myarray($key)
}
By contrast, trying to just go from one element to the next will hurt a lot; that operation is not exposed.
Using the iteration commands is done like this:
set s [array startsearch myarray]
while {[array anymore myarray $s]} {
set key [array nextelement myarray $s]
ProcessElement $key $myarray($key)
}
Note that you don't get an option to sort the search. You won't see these used much in production code; doing array names or array get is usually better. And now (well, 8.7 is still in alpha) you've also got array for:
array for {key value} myarray {
ProcessElement $key $value
}
Efficient for large arrays, but still doesn't permit sorting; supporting direct sorting would require a different sort of storage engine on the back of the array.
This is why it's slow: You're sorting the array names once for the foreach command and then again for each element. Sort once and cache it, then you can iterate over it much more efficiently
set sorted_names [lsort -dictionary [array names refdata -glob {*,Pt,*}]]
set len [llength $sorted_names]
for {set i 0; set j 1} {$i < $len} {incr i; incr j} {
set this_name [lindex $sorted_names $i]
set next_name [lindex $sorted_names $j]
# ...
}

Issue with tcl array index and passing array in function

I have two questions:
In one of my tcl functions an array is declared and initialized as follows:
foreach port $StcPorts {
set dutport [lindex $DutPortsTmp $i]
set STCPort($dutport) [list 0 [lindex [lindex $cardlist $port] 0] [lindex [lindex $cardlist $port] 1]]
}
This is pretty confusing to me since I am new to tcl. Please help me to understand the above code on STCPort creation.
Question 2:
In the same file, STCPort is being used like this:
foreach dutport $DutPortsTmp {
set slot [lindex $STCPort($dutport) 1]
set port [lindex $STCPort($dutport) 2]
set hPort [stc::create port -under $hProject -location //$stcipaddress/$slot/$port -useDefaultHost False ]
lappend STCPort($dutport) $hPort
}
I have sourced this file and my requirement is to get the hPort value in another function residing in another file and work on it. Below is the function d_stc:
proc d_stc {args} {
global DutPorts STCPort
upvar 1 $STCPort _STCPort
#I am trying to get hPort by array index below:
foreach DutPort $DutPorts {
set card [lindex $_STCPort($DutPort) 1]
set port [lindex $_STCport($DutPort) 2]
set hport [lindex $_STCPort($DutPort) 3]
}
And, I am getting the following error:
can't read "STCPort": variable is array
while executing
"upvar 1 $STCPort _STCPort"
(procedure "d_stc" line 3)
I have used global to access the array in this function. But, where I am going wrong?
Thanks,
foreach port $StcPorts {
set dutport [lindex $DutPortsTmp $i]
set STCPort($dutport) [list 0 [lindex [lindex $cardlist $port] 0] [lindex [lindex $cardlist $port] 1]]
}
This snippet goes through the elements of the list StcPorts, which seem to be integers representing some kind of port numbers. For every port number, an array index (stored as dutport) is generated by taking the ith element from the list DutPortsTmp. The value of i is unexplained by this snippet. Also for everly port number, an array value in the array STCPort is created, with the index created above, and a value constructed like this:
Get the element in the list cardlist that corresponds to the integer index port (call that C)
Form a list from the three items 0, the first subitem in C, and the second subitem in C.
.
foreach dutport $DutPortsTmp {
set slot [lindex $STCPort($dutport) 1]
set port [lindex $STCPort($dutport) 2]
set hPort [stc::create port -under $hProject -location //$stcipaddress/$slot/$port -useDefaultHost False ]
lappend STCPort($dutport) $hPort
}
The list DutPortsTmp, which was used to create array indexes, reappears. We work with every item in the list, with the list item/array index again being named dutport. For every iteration, we look at the corresponding array item, setting slot to the second item of the array item's value (the first item in C, above) and port to the third item of the array item's value (the second item in C). The result of an invocation of stc::create port is assigned to the variable hPort, and the value of the array item is extended with a new list item, the value in hPort.
proc d_stc {args} {
global DutPorts STCPort
upvar 1 $STCPort _STCPort
#I am trying to get hPort by array index below:
foreach DutPort $DutPorts {
set card [lindex $_STCPort($DutPort) 1]
set port [lindex $_STCport($DutPort) 2]
set hport [lindex $_STCPort($DutPort) 3]
}
This should probably be:
proc d_stc {args} {
global DutPorts STCPort
foreach DutPort $DutPorts {
set hport [lindex $STCPort($DutPort) 3]
}
}
The corrected snippet again goes through the array indexes for STCPort, but this time DutPorts is used instead of DutPortsTmp to get the array indexes. The hport variable gets the value of the fourth item in each of the array items.
But: the value in hport is never used or returned anywhere, and the value is overwritten by a new value from each new item looked at.
upvar 1 $STCPort _STCPort
does not work since there is no string value in STCPort. If the procedure is called from the global level, this would work:
upvar 1 STCPort _STCPort
(See the difference? Not the value of STCPort, but the string "STCPort" itself.) You don't need this: the invocation of global DutPorts STCPort already makes the array STCPort visible inside the procedure.

execute tcl commands line by line

I have a file like this:
set position {0.50 0.50}
set visibility false
set text {ID: {entity.id}\n Value: {entity.contour_val}}
And I want to do something similar to source, but I want to use a file handle only.
My current attempt looks like this:
proc readArray {fileHandle arrayName} {
upvar $arrayName arr
set cl 0
while {! [eof $fileHandle]} {
set cl [expr "$cl + 1"]
set line [gets $fileHandle]
if [$line eq {}] continue
puts $line
namespace eval ::__esg_priv "
uplevel 1 {*}$line
"
info vars ::__esg_priv::*
foreach varPath [info vars ::__esg_priv::*] {
set varName [string map { ::__esg_priv:: "" } $varPath]
puts "Setting arr($varName) -> [set $varPath]"
set arr($varName) [set $varPath]
}
namespace delete __esg_priv
}
puts "$cl number of lines read"
}
In place of uplevel I tried many combinations of eval and quoting.
My problem is, it either fails on the lines with lists or it does not actuall set the variables.
What is the right way to do it, if the executed commands are expected to be any valid code.
An extra question would be how to properly apply error checking, which I haven't tried yet.
After a call to
readArray [open "myFile.tcl" r] arr
I expect that
parray arr
issues something like:
arr(position) = 0.50 0.50
arr(text) = ID: {entity.id}\n Value: {entity.contour_val}
arr(visibility) = false
BTW: The last line contains internal {}, which are supposed to make it into the string variables. And there is no intent to make this a dict.
This code works, but there are still some problems with it:
proc readArray {fileHandle arrayName} {
upvar $arrayName arr
set cl 0
while {! [eof $fileHandle]} {
incr cl ;# !
set line [gets $fileHandle]
if {$line eq {}} continue ;# !
puts $line
namespace eval ::__esg_priv $line ;# !
foreach varPath [info vars ::__esg_priv::*] {
set varName [string map { ::__esg_priv:: "" } $varPath]
puts "Setting arr($varName) -> [set $varPath]"
set arr($varName) [set $varPath]
}
namespace delete __esg_priv
}
puts "$cl number of lines read"
}
I've taken out a couple of lines that didn't seem necessary, and changed some lines a bit.
You don't need set cl [expr "$cl + 1"]: incr cl will do.
if [$line eq {}] continue will fail because the [...] is a command substitution. if {$line eq {}} continue (braces instead of brackets) does what you intend.
Unless you are accessing variables in another scope, you won't need uplevel. namespace eval ::__esg_priv $line will evaluate one line in the designated namespace.
I didn't change the following, but maybe you should:
set varName [string map { ::__esg_priv:: "" } $varPath] works as intended, but set varName [namespace tail $varPath] is cleaner.
Be aware that if there exists a global variable with the same name as one of the variables in your file, no namespace variable will be created; the global variable will be updated instead.
If you intend to use the value in the text variable as a dictionary, you need to remove either the \n or the braces.
According to your question title, you want to evaluate the file line by line. If that requirement can be lifted, your code could be simplified by reading the whole script in one operation and then evaluating it with a single namespace eval.
ETA
This solution is a lot more robust in that it reads the script in a sandbox (always a good idea when writing code that will execute arbitrary external code) and redefines (within that sandbox) the set command to create members in your array instead of regular variables.
proc readArray {fileHandle arrayName} {
upvar 1 $arrayName arr
set int [interp create -safe]
$int alias set apply {{name value} {
uplevel 1 [list set arr($name) $value]
}}
$int eval [read $fileHandle]
interp delete $int
}
To make it even more safe against unexpected interaction with global variables etc, look at the interp package in the Tcllib. It lets you create an interpreter that is completely empty.
Documentation: apply, continue, eof, foreach, gets, if, incr, info, interp package, interp, list, namespace, proc, puts, set, string, uplevel, upvar, while

Can we create a list of arrays and how?

I want to create a list and each element of it is an array, similarly to an array of structs in C language.
Can it be done in TCL and how if it can? thanks very much!
I did some try but it failed...
tcl>set si(eid) -1
tcl>set si(core) 0
tcl>set si(time) 0
tcl>lappend si_list "$si"
Error: can't read "si": variable is array
You can't create a list of arrays, but you can create a list of dicts which is functionally the same thing (a mapping from keys to values):
set mylist [list [dict create a 1 b 2] [dict create a 4 b 5]]
puts [dict get [lindex $mylist 1] a]
To do it as arrays you need to use [array get] and [array set] to change the array into a string:
set si(eid) -1
set si(core) 0
set si(time) 0
lappend si_list [array get si]
And to get it back out
array set newsi [lindex $si_list]
puts $newsi(eid)
dicts let you work on the {name value} lists directly.
One way to do this on versions of Tcl that don't include dict is to use upvar.
To do this, add the names of the array variables to your list:
set si(eid) -1
set si(core) 0
set si(time) 0
lappend si_list "si"
Then to get your array back, do this:
upvar #0 [lindex $si_list 0] newsi
puts $newsi(eid)
You could also use the ::struct::record package from tcllib for something like that.