Dynamic bash associative array keys - bash

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

Related

Creating and populating dynamically named arrays in bash

I am creating dynamic arrays which all have different letters in their name. For the purpose of this question, my initial array of letters has been set at a fixed length. However, in my final implementation this letter array will be any length.
For each letter, I construct a string
I declare a new array with that string, making use of eval to evaluate the variable's value within the declare command.
I add some values to the array, again using eval to evaluate any variable values.
Here is the code:
declare -a LETTER_ARRAY=( "A" "B" "C" "D" "E" )
for i in "${LETTER_ARRAY[#]}"
do
name_string="apple${i}"
color0="red"
color1="green"
eval "declare -a ${name_string}_array"
eval "${name_string}_array[0]=$color0"
eval "${name_string}_array[1]=$color1"
done
So, how can I iterate through these dynamic arrays and echo what is in them? I have tried the following:
for i in "${LETTER_ARRAY[#]}"
do
eval "array_name='apple${i}_array'"
echo ${array_name[0]}
done
This has not worked for me. I can confirm that my dynamic arrays were successfully created and populated, as when I echo out a value manually, I get a result:
echo ${appleA_array[0]}
red
A perfect place to use a namereference:
letter_array=(A B C D E)
for i in "${letter_array[#]}"; do
declare -n var="apple${i}_array"
var[0]=red
var[1]=green
done
declare -p appleA_array
would output:
declare -a appleA_array=([0]="red" [1]="green")
how can I iterate through these dynamic arrays and echo what is in them?
With the above:
for i in "${letter_array[#]}"; do
declare -n var="apple${i}_array"
printf "%s\n" "${var[0]}"
done
Notes:
Do not use eval. Eval is evil.
Do not use upper case variables, by convention they are used for exported variables, like COLUMNS, PWD, UID, EUID, LINES. Use lower case variables in your scripts.
Check your scripts with http://shellcheck.net for most common mistakes
But if you are creating a 2d array, then an associative array might be better:
declare -A apple_arrays
letter_array=(A B C D E)
for i in "${letter_array[#]}"; do
apple_arrays[$i,0]=red
apple_arrays[$i,1]=green
done
for i in "${letter_array[#]}"; do
printf "one=%s two=%s\n" "${apple_arrays[$i,0]}" "${apple_arrays[$i,1]}"
done
how can I iterate through these dynamic arrays
echo ${array_name[0]} does not work because array_name is not the name of an array; $array_name is. Therefore, eval "echo \"\${${array_name}[0]}\"" would to the trick.
However, I'd recommend namerefs.
By The way: declare works without eval and is more reliable that way.
#! /usr/bin/env bash
letters=({A..E})
for i in "${letters[#]}"; do
declare -a "apple${i}_array=(red green)"
done
for i in "${letters[#]}"; do
declare -n array="apple${i}_array"
# now you can use `array` as if it was `appleA_array`, `appleB_array`, ...
echo "${array[0]}"
done
Your first line is not bash syntax. If I try the line
declare -a LETTER_ARRAY = [ "A" "B" "C" "D" "E" ]
I get:
bash: declare: `=': not a valid identifier
bash: declare: `[': not a valid identifier
bash: declare: `]': not a valid identifier
I think, you get similar error messages, but you ignored them
More errors:
Forgotten do
case mismatch: suffix on definition: _array, and for output: _ARRAY
Use always double quotes when using [#]
One correct syntax is:
declare -a LETTER_ARRAY=( "A" "B" "C" "D" "E" )
for i in "${LETTER_ARRAY[#]}"
do
name_string="apple${i}"
color0="red"
color1="green"
eval "declare -a ${name_string}_array"
echo "${name_string}_array[0]=$color0"
eval "${name_string}_array[0]=$color0"
eval "${name_string}_array[1]=$color1"
done
echo ${appleA_array[0]}
Your eval "array_name='AZ${i}_ARRAY'" makes array_name a scalar, not an array. Arrays in bash are usually created like this
arr=( your elements go here )
If you want to assign one array to another, you have to interpolate the elements between those parenthesis, for instance:
arr=( ${other_array[#]} )
Since you are using bash, this would perform word splitting the elements of other_array, if they contain spaces. Hence you would usually write it for the safe side as
arr=( "${other_array[#]}" )
Hence, for your case, you could do a
eval "array_name=( \${AZ${i}_ARRAY[#]} )"
This causes an array array_name to be created, with the elements of the respective AZi_ARRAY.
I omitted here for simplicity the prevention against word splitting, because in your example, the array elements contain single words only.

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.

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.

Multidimensional associative arrays in 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.

Resources