how to substitute a variable in an array name? - arrays

Let's assume that thefilevalue_$thefile is an array that contains lists
foreach element [array names thefilevalue_$thefile] {
puts "[lindex $thefilevalue_[subst $thefile]($element) 0]"
}
but it returns :
can't read "thefilevalue_": no such variable
i am in tcl 8.4 and i con't upgrade it.
how can i fix it ?
Thanks

Use set and escape the parentheses, e.g.
array set thefilevalue_test {reds {orange red purple} blues {green blue purple}}
set thefile test
foreach element [array names thefilevalue_$thefile] {
puts [lindex [set thefilevalue_$thefile\($element\)] 0]
}
This outputs for me (Tcl 8.0.5, and I can't upgrade either):
orange
green

Related

Print the content of a dynamic array in tcl

I am trying to create n number of arrays from a file. The arrays are getting created properly when I try to print value of an array specifically by $array_name(PAD), I am getting proper expected values. Now the issue is when I try to print the same value using a variable I am not getting the value.
Eg:
set f "BC1201_MP_PM"
puts ${f}(PAD)
The output should be the content of BC1201_MP_PM(PAD), but the output is "BC1201_MP_PM(PAD)". Can anyone please help.
Thanks in Advance!
Utkarsh
There are a few ways you can do this, some being better at some situations.
Without any context, I'd say the simplest would be to use set or a dictionary as Shawn suggested in the comments:
puts [set ${f}(PAD)]
# OR
dict set mydata BC1201_MP_PM PUB foo
puts [dict get $mydata $f PUB]
If it was a situation where you need to print all the array's contents, then you might as well use a loop with array get:
foreach {k v} [array get $f] {
puts "The value for $k is $v"
}
In a proc, you may use upvar and pass the array name to the proc like so:
proc foo {arrayName} {
upvar $arrayName localArray
puts $localArray(PAD)
}
References: set, array get, upvar

Obtaining array values in Tcl

I have tcl arrays having the names as 0,1,2,.. and the values contain meaningful information.
I want to use a single command like [array names $my_array] to get the values of the array.
Right now I only see this option,(lengthy but gets job done)
for {set index 0} {$index<[array size my_array]} {incr index} {
lappend my_list_values $my_array($index)
}
You can use array get to fetch all the elements of the array, and a foreach loop that consumes multiple elements of its list in each iteration (Plus sorting by key to get a reproducible order):
% set foo(0) a
a
% set foo(1) b
b
% set foo(2) c
c
% foreach {key val} [lsort -integer -stride 2 [array get foo]] { lappend values $val }
% puts $values
a b c
%
Or with array names:
foreach key [lsort -integer [array names foo]] { lappend values $foo($key) }
Here's a proc implementing a foreach functionality for arrays. In Tcl 8.7 this will be builtin: array for
# 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
}
You can add this to the array ensemble:
set map [namespace ensemble configure array -map]
dict set map foreach ::array_foreach
namespace ensemble configure array -map $map
then
% array set a {foo bar baz qux}
% array foreach {key value} a {puts "key=$key, value=$value"}
key=foo, value=bar
key=baz, value=qux

Pass bash array to expect script

I have a bash script that calls an expect script like so
$SCRIPTS_DIRECTORY/my_expect_script.sh $my_bash_array
I can pass a variable over, it seems, and use it.
For this example the variable seems to be in [lindex $argv 0].
From bash, it will be a bunch of values, e.g. 1 2 3 4 5.
I am trying to figure out how to use expect to grab this variable, save it to an array then loop through the array to spit out multiple commands one at a time.
So my output should be something like
send command 1 \r
send command 2 \r
etc., until it reaches the end of the array.
I thought in expect I would assign it like
array set myArray [lindex $argv 0]
but it looks like I am wrong.
Would anyone have any good places I can look that might explain going from bash to expect better, or know how to do this? I assume its relatively simple, but expect is very iffy with me in some aspects.
Sample.sh
my_array=(1 2 3 4 5)
expect sample.exp "${my_array[#]}"
Sample.exp
foreach arg $argv {
puts "arg : $arg"
}
Output :
dinesh#mypc:~$ ./sample.sh
arg : 1
arg : 2
arg : 3
arg : 4
arg : 5
dinesh#mypc:~$
Expanding on #glenn jackman's comment...
Expect treats an Array as a set of key-pair values, while Expect treats a List simply as a list.
Therefore to be able to pass an array/list as a single arg...
Array Example (myExpectArray.tcl)
array set myArray [lindex $argv 0];
foreach item [array name myArray] {
puts $myArray($item)
}
Call the expect script from command line...
myExpectArray.tcl "0 dog 1 cat 2 rabbit"
To call this from bash, you'd need to loop through the array and build the string to pass to the expect script. Using a List is easier...
List Example (myExpectList.tcl)
set myList [lindex $argv 0];
for {set i 0} {$i < [llength $myList]} {incr i} {
puts [lindex $myList $i]
}
Call the expect script from bash as follows...
myAnimals+=( "dog" "cat" "rabbit" )
myArgs=${myAnimals[#]}
myExpectList.tcl "$myArgs"
Note: You must convert the list ($myAnimals) to a string ($myArgs) otherwise Expect treats each element of the array as a unique arg.

How to print multiple arrays in TCL?

#!/usr/bin/expect -f
set myarr1(chicken) animal
set myarr1(cows) animal
set myarr1(tiger) animal
set myarr1(horse) animal
set myarr2(carrot) vegetable
set myarr2(tomato) vegetable
set myarr2(potato) vegetable
set myarr2(pea) vegetable
set arr_list { myarr1 myarr2 }
foreach key [array names [lindex $arr_list 0]] {
puts "${key}=$[lindex $arr_list 0]($key)"
}
foreach key [array names [lindex $arr_list 1]] {
puts "${key}=$[lindex $arr_list 1]($key)"
}
Output obtained:
cows=$myarr1(cows)
horse=$myarr1(horse)
chicken=$myarr1(chicken)
tiger=$myarr1(tiger)
tomato=$myarr2(tomato)
pea=$myarr2(pea)
potato=$myarr2(potato)
carrot=$myarr2(carrot)
Required output:
cows=animal
horse=animal
chicken=animal
tiger=animal
tomato=vegetable
pea=vegetable
potato=vegetable
carrot=vegetable
I am able to get the required output if I use the following in foreach loop:
foreach key [array names myarr1] {
puts "${key}=$myarr1($key)"
}
foreach key [array names myarr2] {
puts "${key}=$myarr2($key)"
}
I am trying to create a list of array names and then loop through that list of array names and print it. If there is a better way to approach this problem, I am all ears. Thanks for the assist !
You really ought to use nested foreachs for that.
foreach arr $arr_list {
foreach key [array names $arr] {
puts "${key}=$$arr($key)"
}
}
Except that doesn't work! Why? It's easy on one level: the syntax for $ doesn't support such complexity; it really only supports a (very useful) subset of legal variable names and can't do complicated substitutions (array element names support more diverse options). We need to rewrite to use the single-argument set form as a first step:
foreach arr $arr_list {
foreach key [array names $arr] {
puts "${key}=[set [set arr]($key)]"
}
}
That works, but isn't very elegant (or fast, for that matter). It's actually better to use upvar 0 to make a local alias to the array that you're processing; variable alias from a to whatever was talked about with $arr will let us shorten things elsewhere, and it's pretty elegant in practice:
foreach arr $arr_list {
upvar 0 $arr a
foreach key [array names a] {
puts "$key=$a($key)"
}
}
You can also do things like sorting the list of element names (array names does not guarantee to return things in any particular order), putting spacing in between each of the arrays that you print out, etc. But that's the core of how to improve things. You can also use array get instead of array names and a multi-variable foreach, but then sorting the keys is more awkward (well, before Tcl 8.6's lsort gained the -stride option).
If you just want to print them, use the bundled parray proc:
foreach arr $arr_list {
parray $arr
}
which outputs
myarr1(chicken) = animal
myarr1(cows) = animal
myarr1(horse) = animal
myarr1(tiger) = animal
myarr2(carrot) = vegetable
myarr2(pea) = vegetable
myarr2(potato) = vegetable
myarr2(tomato) = vegetable
You can use the foreach command to traverse both the keys and the values in the array.
array set arr {a 1 b 2 c 3}
foreach {k v} [array get arr] {
puts $k=$v
}
References: foreach
You're on the right track with the foreach command, but to loop over the list of arrays you need to nest it:
foreach arr $arr_list {
foreach {key val} [array get arr] {
puts "$key = $val"
}
}
Also, look into the parray command, and see if that style of printing works for what you need. (I'd link it, but the site appears to be down at the moment. I'll try to remember to edit this later. Check out the command details at http://www.tcl.tk/man/tcl8.6/)
What you probably want is:
foreach arr $arr_list {
foreach key [array names $arr] {
puts "$key=[set ${arr}($key)]"
}
}
That said, you’re using arrays in a non-standard way that possibly is suboptimal. Have you looked into dictionaries? (Unfortunately, discussing how your data should be structured isn’t a Stackoverflow topic.)

Changing item in list by index in TCL

Im trying to work with this awful language and I have to change an item of list by accessing to it with index
set FileRead [open "$flPath" r]
set Data [read $FileRead]
set DataList [split $Data "\n"] #Guess that it creates a list, not an array right?
for {set i 1} { $i < [llength $DataList]} {incr i} {
set line [lindex $DataList $i]
#Some changes on $line
lreplace $DataList $i $line # Thought this should replace the DataList[$i] with my $line
}
I dont understand the awful syntax of TCL! How can I realise it?
You shouldn't just call a language awful because you don't know how to use it, otherwise, just don't use it at all...
In any case, you should read the documentation of lreplace. You supply:
lreplace list first last ?element element ...?
To replace the line in the list of DataList, you thus should change your line to:
lreplace $DataList $i $i $line
This will replace the ith item in DataList with the string in line.
However, this will not change the list DataList at all you need to set the result of the lreplace to a variable (lreplace does not alter the list DataList directly) like...
set DataList [lreplace $DataList $i $i $line]
Though if you are changing only one item in the list at a time, it would be better to use lset:
set FileRead [open "$flPath" r]
set Data [read $FileRead]
set DataList [split $Data "\n"] #Guess that it creates a list, not an array right?
for {set i 1} { $i < [llength $DataList]} {incr i} {
set line [lindex $DataList $i]
# Some changes on $line
lset DataList $i $line
}
lset unlike lreplace does not need to be set since it changes the list in addition to returning the substituted list.
If you are changing every item in a list, and the new value can be produced by a simple script, lmap (list map) is the way to go. The command creates a list with the same number of items as the original list, where each item in the new list has the value of the script with the value of the corresponding item in the original list inserted. Say you want to count characters for each item in a list:
set list [list can be produced]
lmap item $list {
list $item [string length $item]
}
# => {{can 3} {be 2} {produced 8}}
Note that this command does not change the value of list: you need to assign it back to update the value.
The linsert (list insert) adds new items to a list without removing any of the existing elements:
set list [list can be produced]
linsert $list 1 not
# => {can not be produced}
The lreplace (list replace) is mostly useful for insertions that change the number of elements in a list:
set list [list can be produced]
lreplace $list 0 1 can sometimes but not always be
# => {can sometimes but not always be produced}
set list [list the new value can be produced]
lreplace $list 1 1
# => {the value can be produced}
Again, the value of list isn't changed by linsert or lreplace.
If you want to change an item in a list in place, lset (list set) is your friend:
set list [list the new value can be produced]
lset list 1 correct
# => {the correct value can be produced}
This command does change the value of list, because it belongs to the family of setting commands that take a variable name rather than a variable value, and modify the value in some way.
Documentation: linsert, list, lmap, lreplace, lset, set, string

Resources