shell script associate array value overwriting - bash

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

Related

Change name of Variable while in a loop

I have this idea in mind:
I have this number: CN=20
and a list=( "xa1-" "xa2-" "xb1-" "xb2-")
and this is my script:
for a in "${list[#]}"; do
let "CN=$(($CN+1))"
echo $CN
Output:
21
22
23
24
I am trying to create a loop where it creates the following variables, which will be referenced later in my script:
fxp0_$CN="fxp-$a$CN"
fxp0_21="fxp-xa1-21"
fxp0_22="fxp-xa2-22"
fxp0_23="fxp-xb1-23"
fxp0_24="fxp-xb2-24"
However, I have not been able to find a way to change the variable name within my loop. Instead, I was trying myself and I got this error when trying to change the variable name:
scripts/srx_file_check.sh: line 317: fxp0_21=fxp0-xa2-21: command not found
After playing around I found the solution!
for a in "${list[#]}"; do
let "CN=$(($CN+1))"
fxp_int="fxp0-$a$CN"
eval "fxp0_$CN=${fxp_int}"
done
echo $fxp0_21
echo $fxp0_22
echo $fxp0_23
echo $fxp0_24
echo $fxp0_25
echo $fxp0_26
echo $fxp0_27
echo $fxp0_28
Output:
fxp0-xa1-21
fxp0-xa2-22
fxp0-xb1-23
fxp0-xb2-24
fxp0-xc1-25
fxp0-xc2-26
fxp0-xd1-27
fxp0-xd2-28
One common method for maintaining a dynamically generated set of variables is via arrays.
When the variable names vary in spelling an associative array comes in handy whereby the variable 'name' acts as the array index.
In this case since the only thing changing in the variable names is a number we can use a normal (numerically indexed) array, eg:
CN=20
list=("xa1-" "xa2-" "xb1-" "xb2-")
declare -a fxp0=()
for a in "${list[#]}"
do
(( CN++ ))
fxp0[${CN}]="fxp-${a}${CN}"
done
This generates:
$ declare -p fxp0
declare -a fxp0=([21]="fxp-xa1-21" [22]="fxp-xa2-22" [23]="fxp-xb1-23" [24]="fxp-xb2-24")
$ for i in "${!fxp0[#]}"; do echo "fxp0[$i] = ${fxp0[$i]}"; done
fxp0[21] = fxp-xa1-21
fxp0[22] = fxp-xa2-22
fxp0[23] = fxp-xb1-23
fxp0[24] = fxp-xb2-24
As a general rule can I tell you that it's not a good idea to modify names of variables within loops.
There is, however, a way to do something like that, using the source command, as explained in this URL with some examples. It comes down to the fact that you treat a file as a piece of source code.
Good luck

Variable in variablenames

i have a a couple of variables with a number in its names. e.g
SERVER_IP48_SUBNET
..
SERVER_IP60_SUBNET
And an additional variable
SERVER_IP
Im trying to expand/concatenate them in the following way:
ALLIPs=${SERVER_IP}
for i in {48..64}; do
ALLIPs=${ALLIPs},${SERVER_IP${i}_SUBNET}
done
as you can imagine this script fails saying:
Wrong substitution
Does anybody of you know a good solution for this problem?
Thanks so far
Use a nameref with bash version 4.3 +
ALLIPs=${SERVER_IP}
for i in {48..64}; do
declare -n tmp="SERVER_IP${i}_SUBNET"
ALLIPs+=",$tmp"
done
But you should really be using an array in the first place:
server_ip=0.0.0.0
subnet_ip=(
[48]=1.1.1.1
[49]=2.2.2.2
# ...
[64]=16.16.16.16
)
all_ips=( "$server_ip" )
for i in {48..64}; do
all_ips+=( "${subnet_ip[i]}" )
done
(
IFS=,
echo "ALLIPs = ${all_ips[*]}"
)
Get out of the habit of using ALLCAPS variable names, leave those as
reserved by the shell. One day you'll write PATH=something and then
wonder why
your script is broken.
I just noticed, if you just want a to join the IP addresses with commas, and you're using an array, you don't need a loop at all:
all_ips=$(
IFS=,
set -- "$server_ip" "${subnet_ip[#]}"
echo "$*"
)
You can use ${!varprefix#} or ${!varprefix*} to expand to all variables with that common prefix (the difference is the same as $# and $*):
SERVER_IP48_SUBNET=48sub
SERVER_IP49_SUBNET=49sub
SERVER_IP50_SUBNET=50sub
SERVER_IP=1.2.3.4
# set this as empty since !SERVER_IP# also matches SERVER_IP
ALLIPS=""
for var in "${!SERVER_IP#}"; do
ALLIPS=$ALLIPS,${!var}
done
This would probably be more practical if you could invert the names like this, since we can only match prefixes:
SERVER_IP_SUBNET_48=48sub
SERVER_IP_SUBNET_49=49sub
SERVER_IP_SUBNET_50=50sub
SERVER_IP=1.2.3.4
ALLIPS=$SERVER_IP
for var in "${!SERVER_IP_SUBNET_#}"; do
ALLIPS=$ALLIPS,${!var}
done
More info on this feature in the bash manual.
One idea:
SERVER_IP48_SUBNET=48sub
SERVER_IP49_SUBNET=49sub
SERVER_IP50_SUBNET=50sub
SERVER_IP=1.2.3.4
ALLIPs=${SERVER_IP}
for i in {48..50}
do
tmpvar="SERVER_IP${i}_SUBNET" # build the variable name
ALLIPs="${ALLIPs},${!tmpvar}" # indirect variable reference via tmpvar
done
echo "ALLIPs = $ALLIPs}"
This generates:
ALLIPs = 1.2.3.4,48sub,49sub,50sub

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
...

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.

Can't access global associate array using indirect expansion?

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.

Resources