Multidimensional associative arrays in Bash - bash

I'm trying to create a multidimensional associative array but need some help. I have reviewed the page suggested in this SO answer but it confused me even more. So far here is what I have:
The script:
#!/bin/bash
declare -A PERSONS
declare -A PERSON
PERSON["FNAME"]='John'
PERSON["LNAME"]='Andrew'
PERSONS["1"]=${PERSON[#]}
PERSON["FNAME"]='Elen'
PERSON["LNAME"]='Murray'
PERSONS["2"]=${PERSON[#]}
for KEY in "${!PERSONS[#]}"; do
TMP="${PERSONS["$KEY"]}"
echo "$KEY - $TMP"
echo "${TMP["FNAME"]}"
echo "${TMP["LNAME"]}"
done
The output:
1 - John Andrew
John Andrew
John Andrew
2 - Elen Murray
Elen Murray
Elen Murray
As you can see trying to access a specific index of the $TMP array in the for loop returns the whole array.
[Q] What do I need to do in order to separately access the "FNAME" and "LNAME" indexes of the $TMP array inside the for loop?
Thanks.

You can't do what you're trying to do: bash arrays are one-dimensional
$ declare -A PERSONS
$ declare -A PERSON
$ PERSON["FNAME"]='John'
$ PERSON["LNAME"]='Andrew'
$ declare -p PERSON
declare -A PERSON='([FNAME]="John" [LNAME]="Andrew" )'
$ PERSONS[1]=([FNAME]="John" [LNAME]="Andrew" )
bash: PERSONS[1]: cannot assign list to array member
You can fake multidimensionality by composing a suitable array index string:
declare -A PERSONS
declare -A PERSON
PERSON["FNAME"]='John'
PERSON["LNAME"]='Andrew'
i=1
for key in "${!PERSON[#]}"; do
PERSONS[$i,$key]=${PERSON[$key]}
done
PERSON["FNAME"]='Elen'
PERSON["LNAME"]='Murray'
((i++))
for key in "${!PERSON[#]}"; do
PERSONS[$i,$key]=${PERSON[$key]}
done
declare -p PERSONS
# ==> declare -A PERSONS='([1,LNAME]="Andrew" [2,FNAME]="Elen" [1,FNAME]="John" [2,LNAME]="Murray" )'

I understand what you need. I also wanted the same for weeks.
I was confused whether to use Python or Bash.
Finally, exploring something else I found this
Bash: How to assign an associative array to another variable name (e.g. rename the variable)?
Here, I got to know how to assign some string and use it later as command.
Then with my creativity I found solution to your problem as below:-
#!/bin/bash
declare -A PERSONS
declare -A PERSON
PERSON["FNAME"]='John'
PERSON["LNAME"]='Andrew'
string=$(declare -p PERSON)
#printf "${string}\n"
PERSONS["1"]=${string}
#echo ${PERSONS["1"]}
PERSON["FNAME"]='Elen'
PERSON["LNAME"]='Murray'
string=$(declare -p PERSON)
#printf "${string}\n"
PERSONS["2"]=${string}
#echo ${PERSONS["2"]}
for KEY in "${!PERSONS[#]}"; do
printf "$KEY - ${PERSONS["$KEY"]}\n"
eval "${PERSONS["$KEY"]}"
printf "${PERSONS["$KEY"]}\n"
for KEY in "${!PERSON[#]}"; do
printf "INSIDE $KEY - ${PERSON["$KEY"]}\n"
done
done
OUTPUT:-
1 - declare -A PERSON='([FNAME]="John" [LNAME]="Andrew" )'
declare -A PERSON='([FNAME]="John" [LNAME]="Andrew" )'
INSIDE FNAME - John
INSIDE LNAME - Andrew
2 - declare -A PERSON='([FNAME]="Elen" [LNAME]="Murray" )'
declare -A PERSON='([FNAME]="Elen" [LNAME]="Murray" )'
INSIDE FNAME - Elen
INSIDE LNAME - Murray
The problem actually with multi dimensional arrays in bash and specifically in your approach is that you are assigning PERSON array values to the array element PERSONS[1] which is converted to a list and not an assoc array when you assigned it.
And so it no longer will take it as 2 elements of an array as you are not keeping any info about the array data structure in your value.
So, I found this hack to be sufficient with only 1 limitation that you will have to do this each time you want to do store/retrieve values. But it shall solve your purpose.

Related

Adding to Bash associative arrays inside functions

I'm trying to use associative arrays as a work around for Bash's poor function parameter passing. I can declare a global associative array and read/write to that but I would like to have the variable name passed to the function since many times I want to use the same function with different parameter blocks.
Various Stack Overflow posts have approaches for reading a passed array within a function but not writing to it to allow return values. Pseudo Bash for what I'm trying to do is thus:
TestFunc() {
local __PARMBLOCK__=${1} # Tried ${!1} as well
# Do something with incoming array
__PARMBLOCK__[__rc__]+=1 # Error occured
__PARMBLOCK__[__error__]+="Error in TestFunc"
}
declare -A FUNCPARM
# Populate FUNCPARM
TestFunc FUNCPARM
if [[ ${FUNCPARM[__rc__]} -ne 0 ]]; then
echo "ERROR : ${FUNCPARM[__error__]}
fi
Is this kind of thing possible or do I really need to abandon Bash for something like Python?
EDIT: Found the duplicate. This is basically the same answer as this one.
You can use a reference variable for that, see help declare:
declare [-aAfFgilnrtux] [-p] [name[=value] ...]
[...]
-n make NAME a reference to the variable named by its value
[...]
When used in a function, declare makes NAMEs local, as with the local command.
f() {
declare -n paramblock="$1"
# example for reading (print all keys and entries)
paste <(printf %s\\n "${!paramblock[#]}") <(printf %s\\n "${paramblock[#]}")
# example for writing
paramblock["key 1"]="changed"
paramblock["new key"]="new output"
}
Example usage:
$ declare -A a=(["key 1"]="input 1" ["key 2"]="input 2")
$ f a
key 2 input 2
key 1 input 1
$ declare -p a
declare -A a=(["key 2"]="input 2" ["key 1"]="changed" ["new key"]="new output" )
This works very well. The only difference to an actual associative array I found so far is, that you cannot print the referenced array using declare -p as that will only show the reference.

Dynamic bash associative array keys

I've been wrestling with this problem for a long time and can't seem to find a working solution.
Suppose I have declared two associative arrays.
declare -A FOO_ARRAY=(
[a]="first"
[b]="second"
[c]="third"
)
declare -A BAR_ARRAY=(
[a]="first"
[b]="second"
[c]="third"
)
I can get a list of keys like so:
$ echo ${!FOO_ARRAY[#]}
c b a
$ echo ${!BAR_ARRAY[#]}
c b a
I can also dynamically deference a key from the array by doing something like this:
for KEY in FOO BAR
do
temp="${KEY}_ARRAY[a]"
echo ${!temp}
done
However, if you notice that the operator for dynamically referencing variables is also the same to get the list of keys from an associative array, how could I make something like this possible in order to dynamically list the keys in an associative array?
A naive example like this results in an invalid variable name error.
$ export NAME=foo
$ export temp="\!${NAME^^}_ARRAY[#]"
$ echo ${!temp}
bash: \!FOO_ARRAY[#]: invalid variable name
What I'm searching for is a way to dynamically return the list of keys from an associative array as if the above code sample returned:
c b a
You can use a nameref:
declare -rgA FOO_ARRAY=(
[a]="first"
[b]="second"
[c]="third"
)
name="foo"
declare -n array="${name^^}_ARRAY"
echo "The keys are: " "${!array[#]}"
Use declare -n. From help declare:
-n make NAME a reference to the variable named by its value
Example:
declare -A foo_array=(
[a]="first"
[b]="second"
[c]="third"
)
declare -n temp="foo_array"
echo "${!temp[#]}" # -> a b c
For more details, see info bash under the "Bash Builtins" section.
This Will Work
NAME=foo
temp="${NAME^^}_ARRAY[#]"
echo ${!temp}
Explanation: here temp will store " FOO_ARRAY[#] " as string only and when echo it in reference way it does print the value.
Example
declare -rgA FOO_ARRAY=(
[a]="first"
[b]="second"
[c]="third"
)
declare -rgA BAR_ARRAY=(
[a]="first"
[b]="second"
[c]="third"
)
echo ${!FOO_ARRAY[#]}
echo ${!BAR_ARRAY[#]}
for KEY in FOO BAR
do
temp="${KEY}_ARRAY[a]"
echo ${!temp}
done
NAME=foo
temp="${NAME^^}_ARRAY[#]"
echo ${!temp}
Run:
a b c
a b c
first
first
first second third

Open file with two columns and dynamically create variables

I'm wondering if anyone can help. I've not managed to find much in the way of examples and I'm not sure where to start coding wise either.
I have a file with the following contents...
VarA=/path/to/a
VarB=/path/to/b
VarC=/path/to/c
VarD=description of program
...
The columns are delimited by the '=' and some of the items in the 2nd column may contain gaps as they aren't just paths.
Ideally I'd love to open this in my script once and store the first column as the variable and the second as the value, for example...
echo $VarA
...
/path/to/a
echo $VarB
...
/path/to/a
Is this possible or am I living in a fairy land?
Thanks
You might be able to use the following loop:
while IFS== read -r name value; do
declare "$name=$value"
done < file.txt
Note, though, that a line like foo="3 5" would include the quotes in the value of the variable foo.
A minus sign or a special character isn't allowed in a variable name in Unix.
You may consider using BASH associative array for storing key and value together:
# declare an associative array
declare -A arr
# read file and populate the associative array
while IFS== read -r k v; do
arr["$k"]="$v"
done < file
# check output of our array
declare -p arr
declare -A arr='([VarA]="/path/to/a" [VarC]="/path/to/c" [VarB]="/path/to/b" [VarD]="description of program" )'
What about source my-file? It won't work with spaces though, but will work for what you've shared. This is an example:
reut#reut-home:~$ cat src
test=123
test2=abc/def
reut#reut-home:~$ echo $test $test2
reut#reut-home:~$ source src
reut#reut-home:~$ echo $test $test2
123 abc/def

How to get a last key of an associative array (dictionary)

How I can have an access to the last key of the associative array in bash? In this example I need to have "lot" in the $last variable. I found a way described here: How to get the keys and values of an associative array indirectly in Bash?. But it doesn't work as expected in the example below and return this error:
./test.sh: line 9: keys2: ${!$addict[#]}: must use subscript when assigning associative array
Here are the contents of this test.sh:
declare -A addict=(
["foo"]="bar"
["few"]="baz"
["lot"]="pot"
)
index_last=$(( ${#addict[#]} - 1 ))
eval 'declare -A keys2=(${!$addict[#]})'
last="${keys2[$index_last]}"
echo "$keys2"
echo "$index_last"
echo "$last"
While Tom Fenech is absolutely right saying
The keys are unordered, so the concept of a "last key" doesn't really make sense, you can avoid the error by changing the line with eval to
keys2=( "${!addict[#]}" )
and see what you get. It may also be illuminating to look at declare -p addict. In order to get some key (first key that is returned from unordered keys list, not first key that was declared) you can do:
some_key="${keys2[0]}"
This way you could, for example, unset A[$some_key] and iterate over keys in such manner by picking first returned key each time.
Example:
$ declare -A A=( [a]=x [b]=y [c]=z )
$ echo "${!A[#]}"
c b a
$ keys=( "${!A[#]}" )
$ echo "${keys[0]}"
c
# you see that c was returned instead of a
# (on your computer order could be different)
$ unset A[$some_key]
$ echo "${!A[#]}"
b a
$ declare -p A
declare -A A=([b]="y" [a]="x" )
Further reading: GNU Bash - Arrays.

Creating array of objects in bash

Is it possible to create an array of objects in bash?
That's how I'm trying:
declare -a identifications=(
{
email = '...',
password = '...'
}
)
declare -a years=(
'2011'
'2012'
'2013'
'2014'
'2015'
'2016'
)
for identification in "${identifications[#]}"
do
for year in "${years[#]}"
do
my_program --type=CNPJ --format=XLS --identification=${identification.email} --password=${identication.password} --competence=${year} --output="$identification - $year"
done
done
Obviously, this doesn't work, and I'm not finding how to achieve that, since I'm not finding bash objects.
You could do some trickery with associative arrays (introduced in Bash 4.0) and namerefs (see manual for declare and the first paragraph of Shell Parameters – introduced in Bash 4.3):
#!/usr/bin/env bash
declare -A identification0=(
[email]='test#abc.com'
[password]='admin123'
)
declare -A identification1=(
[email]='test#xyz.org'
[password]='passwd1!'
)
declare -n identification
for identification in ${!identification#}; do
echo "Email: ${identification[email]}"
echo "Password: ${identification[password]}"
done
This prints
Email: test#abc.com
Password: admin123
Email: test#xyz.org
Password: passwd1!
declare -A declares an associative array.
The trick is to assign all your "objects" (associative arrays) variable names starting with the same prefix, like identification. The ${!prefix#} notation expands to all variable names starting with prefix:
$ var1=
$ var2=
$ var3=
$ echo "${!var#}"
var1 var2 var3
Then, to access the key-value pairs of the associative array, we declare the control variable for the for loop with the nameref attribute:
declare -n identification
so that the loop
for identification in ${!identification#}; do
makes identification behave as if it were the actual variable from the expansion of ${!identification#}.
In all likelihood, it'll be easier to do something like the following, though:
emails=('test#abc.com' 'test#xyz.org')
passwords=('admin123' 'passwd1!')
for (( i = 0; i < ${#emails[#]}; ++i )); do
echo "Email: ${emails[i]}"
echo "Password: ${passwords[i]}"
done
I.e., just loop over two arrays containing your information.
I tend to use json to create objects. For me it makes it really easy and flexible.
Here is a oversimplified example.
I create a json file: devices.json
{
"backup" : [{
"addr":"192.168.1.1",
"username":"backuper",
"dstfile":"firewallconfig",
"ext":".cfg",
"method":"ssh",
"rotate":"no",
"enabled":"yes"
}, {
"addr":"192.168.1.2",
"username":"backuper",
"dstfile":"routerconfig",
"ext":".cfg",
"method":"ssh",
"rotate":"no",
"enabled":"yes"
}]
}
Bash script: task.sh
# read the devices.json file and store it in the variable jsonlist
jsonlist=$(jq -r '.backup' "devices.json")
# inside the loop, you cant use the fuction _jq() to get values from each object.
for row in $(echo "${jsonlist}" | jq -r '.[] | #base64'); do
_jq()
{
echo ${row} | base64 --decode | jq -r ${1}
}
echo "backing up: $(_jq '.addr')"
echo "using method: $(_jq '.method')"
Done
The guy who posted the original post can be found by googling "using json with bash" or something.

Resources