Can't access global associate array using indirect expansion? - bash

I have the following setup:
#! /bin/bash
init_globals() {
declare -gA global_arr1=( ["key"]="val" )
}
init_globals
echo "${global_arr1["key"]}" # WORKS! print val
local_arr1=( ["key"]="local val" )
i=1
temp=local_arr$i
current_arr=${!temp}
echo ${current_arr["key"]} # WORKS! print local val
temp=global_arr$i
current_arr=${!temp}
echo ${current_arr["key"]} # DOESN'T WORK! expect val but print nothing...
I'm trying to access globally defined associative array, based on the variable i. So I use indirect expansion to assign current_arr to what I want. It works perfectly for an associative array defined locally. But it doesn't work with global array. Why so?

You didn't declare local_arr1 as an associative array. It springs into existence with
local_arr1=( [key]="local val" )
so bash creates a normal array for you, with key understood as a variable whose value is the index in the array (zero in this case, as there's no $key). You can test it with set -eu or key=1.
Note that the correct way to use indirection on arrays is to include the index in the string:
arr1[2]=x
i=1
j=2
tmp=arr$i[$j]
echo ${!tmp}

It is because:
local_arr1=( ["key"]="local val" )
is not really an associative array. You can check by:
declare -p local_arr1
which prints:
declare -a local_arr1='([0]="local val")'
If you use it right:
declare -A local_arr1=( ["key"]="local val" )
then behavior will be same for both arrays.

Related

Two-Way Hash in Bash?

I call a binary [used to set the state of an external device over IP] from a bash script with arguments that are not (easily) human readable (meaningful). e.g. "video2,cbl,sat"
Consequently, I call the bash script with a more user-friendly argument, e.g. "amazon", and use an associative array to get the unfriendly argument:
declare -A state=( [amazon]="video2,cbl,sat" )
input_arg=${state[amazon]}
/usr/bin/set_state source:"$input_arg"
This is fine when I set state, but I also need to get state and return this to the [human] user, so I have the reverse hash:
declare -A current_state=( [video2,cbl,sat]="amazon" )
output=$(/usr/bin_get_state)
friendly_output=${current_state["$output"]}
echo "$friendly_output"
Is there a way to have a two-way hash in bash without maintaining two such arrays?
The same array could be used to store maps in both directions. It would work, but it doesn't feel quite right!
Bash doesn't provide any means to invert a hash, so you need to iterate it yourself key by key.
#!/bin/bash
declare -A state
state=([amazon]="video2,cbl,sat"
[netflix]="video3,cbl,inet")
declare -A current_state
for key in "${!state[#]}" ; do
current_state["${state[$key]}"]=$key
done
You might need to verify the values are unique:
for key in "${!state[#]}" ; do
if [[ ${current_state["${state[$key]}"]} ]] ; then # Fix your syntax HL, SO! "
echo Duplicate "$key". >&2
exit 1
fi
...

Setting a local array to its value

I have these two functions:
function two() {
local -a -x var=( ${var[#]} )
echo "${var[#]}"
}
function one() {
local -a -x var=(11 22 33)
two
}
If I call one, then nothing is printed. Why is that?
nothing is print. Why is that?
Here you're having the same identifier name var in both the functions
The var you defined in one could accessed by two because two is called from one. However,
when declaring and setting a local variable in a single command,
apparently the order of operations is to first set the variable, and
only afterwards restrict it to local scope.
So in
local -a -x var=( "${var[#]}" )
the ${var[#]} part will be empty as the variable var is set local first.
To verify this you could change the variable name in one to var1 and and in two do
local -a -x var=( "${var1[#]}" ) # var1 though local to one should be accessible here.
You could use #inian's answer as a work-around to pass variables easily and yet not bother about such dark corners in bash.
Your code is not reflecting what are you trying to do! The locals you've defined are only within the scope of the function Yes! but if you are passing it to the other function, pass it as positional arguments "$#". In the function below when you do two "${var[#]}", you are passing the local array as a positional argument array to be used in the other function.
two() {
local -a -x var=( "$#" )
echo "${var[#]}"
}
The argument list "$#" represents the argument list passed to the function two, now from the function one pass it as
one() {
local -a -x var=(11 22 33)
two "${var[#]}"
}
Also the use of function keyword is non-standard. POSIX does not recommend using it. If you are planning to re-use script for multiple shells, drop the keyword. Also quote the variables/array to avoid them being string-splited and glob-expanded. It could result in unexpected values in the final array.
Also worth noting that variables/arrays are global by default unless you override with local keyword inside a function.
$ x=2
$ test_local(){ local x=1; }
$ test_local; echo "$x"
2
But the same without local would print the value as 1 which proves the point explained above.
When you declare var in two that declaration hides the one in one. Curiously, local variables are visible in called functions. The easiest way to make this work is to do nothing: simply access $var in two.
two() {
echo "${var[#]}"
}
one() {
local var=(11 22 33)
two "${var[#]}"
}
I don't necessarily recommend doing this, though. It makes it hard to understand what two does just by reading it. It's better to explicitly pass the values as arguments.
two() {
local var=("$#")
echo "${var[#]}"
}
one() {
local var=(11 22 33)
two "${var[#]}"
}
By the way, you should always quote your variable expansions to prevent them from being subjected to word splitting and globbing. In your original code you should quote ${var[#]}:
local -a -x var=( "${var[#]}" )
Also, for portability you should either write one() or function one, but not both. I prefer the former.

Bash declare/init variables from array using a reference

The following code snippet will try to initialize the variables in the arrVAR_INIT array :
#!/bin/bash
set -u
declare -a arrVAR_INIT=(
VERBOSE=FALSE
DEBUG=FALSE
MEMORY="1024k"
DEBUGFILE=
)
# declare -A arrVAR_DEFAULT_VALUE
for VAR in "${arrVAR_INIT[#]}"
do
VAR_NAME=${VAR%%=*}
VAR_VALUE="${VAR#*=}"
echo "$VAR : $VAR_NAME = \"$VAR_VALUE\""
#### ERROR: !VAR_NAME: unbound variable
declare $VAR_NAME="$VAR_VALUE"
# eval "arrVAR_DEFAULT_VALUE[${VAR%%=*}]=\"${VAR#*=}\""
done
Please note that, by using the set -u ( treat unset variables as an error, and immediately exit ), the above code will throw the !VAR_NAME: unbound variable error and exit.
What would be the correct way to init the vars though the reference ?
Can it be done without using eval ?
The quick answer is :
declare "$VAR_NAME=$VAR_VALUE"
Know that if you cannot guarantee the content of the variables is safe, this could open code injection vulnerabilities.
Is there a reason you are not using an associative array? You already have an array to start with, why not make it associative and read from it rather than initializing other variables?
declare -A arrVAR_INIT=(
[VERBOSE]=FALSE
[DEBUG]=FALSE
[MEMORY]="1024k"
[DEBUGFILE]=
)
echo "${arrVAR_INIT[VERBOSE]}" # An example of getting a value out of the array.
You can use declare $var_name="$var_value" like this:
#!/bin/bash
set -u
declare -a arrvar_init=(
VERBOSE=FALSE
DEBUG=FALSE
MEMORY="1024k"
DEBUGFILE=
)
# declare -A arrVAR_DEFAULT_VALUE
for var in "${arrvar_init[#]}"
do
var_name=${var%%=*}
var_value=${var#*=}
declare $var_name="$var_value"
declare -p $var_name
done
Avoid using all uppercase names for your variable names to avoid clash with bash ENV variables.

Returning a Dictionary from a Bash Function

I want to have a function in bash, which create a Dictionary as a local variable. Fill the Dictionary with one element and then return this dictionary as output.
Is the following code correct?
function Dictionary_Builder ()
{
local The_Dictionary
unset The_Dictionary
declare -A The_Dictionary
The_Dictionary+=(["A_Key"]="A_Word")
return $The_Dictionary
}
How can I access to the output of the function above? Can I use the following command in bash?
The_Output_Dictionary=Dictionary_Builder()
To capture output of a command or function, use command substitution:
The_Output_Dictionary=$(Dictionary_Builder)
and output the value to return, i.e. replace return with echo. You can't easily return a structure, though, but you might try returning a string that declares it (see below).
There's no need to use local and unset in the function. declare creates a local variable inside a function unless told otherwise by -g. The newly created variable is always empty.
To add a single element to an empty variable, you can assign it directly, no + is needed:
The_Dictionary=([A_Key]=A_Word)
In other words
#!/bin/bash
Dictionary_Builder () {
declare -A The_Dictionary=([A_Key]=A_Word)
echo "([${!The_Dictionary[#]}]=${The_Dictionary[#]})"
}
declare -A The_Output_Dictionary="$(Dictionary_Builder)"
echo key: ${!The_Output_Dictionary[#]}
echo value: ${The_Output_Dictionary[#]}
For multiple keys and values, you need to loop over the dictionary:
Dictionary_Builder () {
declare -A The_Dictionary=([A_Key]=A_Word
[Second]=Third)
echo '('
for key in "${!The_Dictionary[#]}" ; do
echo "[$key]=${The_Dictionary[$key]}"
done
echo ')'
}
declare -A The_Output_Dictionary="$(Dictionary_Builder)"
for key in "${!The_Output_Dictionary[#]}" ; do
echo key: $key, value: ${The_Output_Dictionary[$key]}
done
The answer by #choroba is what I was looking for. However, my dictionary values also had white spaces in them and the above answer didn't work outright. What worked was a minor variation of the above answer.
#!/bin/bash
function Dictionary_Builder() {
declare -A dict=(['title']="Title of the song"
['artist']="Artist of the song"
['album']="Album of the song"
)
echo '('
for key in "${!dict[#]}" ; do
echo "['$key']='${dict[$key]}'"
done
echo ')'
}
declare -A Output_Dictionary="$(Dictionary_Builder)"
for key in "${!Output_Dictionary[#]}" ; do
echo "${key}: '"${Output_Dictionary[$key]}"'"
done
Note the extra single quotes on the 2nd echo line which made it possible to output values with whitespaces in them.

shell script associate array value overwriting

When I run the following shell script always I am getting the output as "grault" for any key.
What would be the problem?
thanks!
#!/bin/bash
declare -a MYMAP
MYMAP=( [foo]=bar [baz]=quux [corge]=grault )
echo ${MYMAP[foo]}
echo ${MYMAP[baz]}
Create an associative array with -A:
declare -A MYMAP
See: help declare
The other answer describes how to do it right, but here's the explanation of why your example behaves as it does.
declare -a creates an indexed array, which should only accept integers for the index. If you provide a string as the index, it will just disregard it and treat it as a 0! (I think this is a poor behavior, it should just give an error).
So this is what your code translated to:
declare -a MYMAP # create indexed array
MYMAP=( [0]=bar [0]=quux [0]=grault )
echo ${MYMAP[0]} # grault
echo ${MYMAP[0]} # grault

Resources