Iterate over several associative arrays - bash

For my current use case I'm creating an scp script which will copy log files from one server to one or more other servers.
I.e.
server1:/my/path1/log-files.* --> log_server1:/log/path1/server1
server1:/my/path2/log-files.* --> log_server2:/log/path/server1
server1:/my/path3/log-files.* --> log_server1:/log/path2/server1
I would like to be able to use Associative arrays (Arrays) in bash (version 4) for the log file configuration, and loop over all of the A. Arrays by putting their names into an indexed array.
But I'm stumped on how I'm referencing a named A. Array using a variable as the name of the A. Array.
Example:
#!/bin/bash
# GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu) from RedHat/CentOS 6.4
declare -A log_server1 log_server2
log_server1=([name]="ls1" [user]="user")
log_server2=([name]="ls2" [user]="user")
declare -A log1 log2 log3
log1=([log_server]="log_server1" [path]="/my/path1" [file]="log-files" [rpath]="/log/path1/server1")
log2=([log_server]="log_server2" [path]="/my/path2" [file]="log-files" [rpath]="/log/path/server1")
log3=([log_server]="log_server1" [path]="/my/path3" [file]="log-files" [rpath]="/log/path2/server1")
logs=(log1 log2 log3)
for log in ${logs[#]}
do
# How can I now refer to the A. Array by the name of "log1", etc ?
...
done

You can use indirect expansion, but it's really ugly!
#!/bin/bash
declare -A log_server1=([name]="ls1" [user]="user")
declare -A log_server2=([name]="ls2" [user]="user")
declare -A log1=([log_server]="log_server1" [path]="/my/path1" [file]="log-files" [rpath]="/log/path1/server1")
declare -A log2=([log_server]="log_server2" [path]="/my/path2" [file]="log-files" [rpath]="/log/path/server1")
declare -A log3=([log_server]="log_server1" [path]="/my/path3" [file]="log-files" [rpath]="/log/path2/server1")
logs=( log1 log2 log3 )
for log in "${logs[#]}"; do
l_ls=$log[log_server]
l_p=$log[path]
l_f=$log[file]
l_rp=$log[rpath]
echo "array $log:"
echo " log_server => ${!l_ls}"
echo " path => ${!l_p}"
echo " file => ${!l_f}"
echo " rpath => ${!l_rp}"
done
In the reference manual section I linked above, you'll read:
If the first character of parameter is an exclamation point (!), a level of variable indirection is introduced. Bash uses the value of the variable formed from the rest of parameter as the name of the variable; this variable is then expanded and that value is used in the rest of the substitution, rather than the value of parameter itself. This is known as indirect expansion. The exceptions to this are the expansions of ${!prefix} and ${!name[#]} described below. The exclamation point must immediately follow the left brace in order to introduce indirection.
Question. Why don't you, instead, create associative arrays log_server, path, file and rpath with keys log1, log2 and log3? as in:
#!/bin/bash
declare -A log_server1=([name]="ls1" [user]="user")
declare -A log_server2=([name]="ls2" [user]="user")
declare -A log_server path file rpath
log_server[log1]="log_server1"
path[log1]="/my/path1"
file[log1]="log-files"
rpath[log1]="/log/path1/server1"
log_server[log2]="log_server2"
path[log2]="/my/path2"
file[log2]="log-files"
rpath[log2]="/log/path/server1"
log_server[log3]="log_server3"
path[log3]="/my/path3"
file[log3]="log-files"
rpath[log3]="/log/path2/server1"
for log in "${!log_server[#]}"; do
echo "log server $log:"
echo " log_server => ${log_server[$log]}"
echo " path => ${path[$log]}"
echo " file => ${file[$log]}"
echo " rpath => ${rpath[$log]}"
done

Presenting my own answer.
I'm expecting some healthy critisism :-)
However, the main question was how to use identical associative arrays, and looping over them in a unified manner.
Suggestions on how to achieve the same will be greatly appreciated:
#!/bin/bash
# GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu) from RedHat/CentOS 6.4
declare -A log_server1 log_server2
log_server1=([name]="ls1" [user]="user")
log_server2=([name]="ls2" [user]="user")
declare -A clog1 clog2 clog3
clog1=([log_server]="log_server1" [path]="/my/path1" [file]="log-files" [rpath]="/log/path1/server1/")
clog2=([log_server]="log_server2" [path]="/my/path2" [file]="log-files" [rpath]="/log/path/server1/")
clog3=([log_server]="log_server1" [path]="/my/path3" [file]="log-files" [rpath]="/log/path2/server1/")
for log in ${!clog*}
do
l_ls=$log[log_server] ; l_p=$log[path] ; l_f=$log[file] ; l_rp=$log[rpath]
l_ls=${!l_ls} ; l_p=${!l_p} ; l_f=${!l_f} ; l_rp=${!l_rp}
r_n=$l_ls[name] ; r_u=$l_ls[user]
r_n=${!r_n} ; r_u=${!r_u}
echo "Array $log:"
cmd=" scp ${l_p}/${l_f}* ${r_u}#${r_n}:${l_rp}"
echo "${cmd}"
done
Result:
$./bash-A-Array.sh
Array clog1:
scp /my/path1/log-files* user#ls1:/log/path1/server1/
Array clog2:
scp /my/path2/log-files* user#ls2:/log/path/server1/
Array clog3:
scp /my/path3/log-files* user#ls1:/log/path2/server1/

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

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.

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

Accessing bash array keys (mac)

For some reason, I am unable to access the array keys with the exclamation point syntax:
declare -a sites
sites=(["fr"]="frederick" ["an"]="annapolis")
for i in "${!sites[#]}"
do
echo "key: $i "
done
This Just echo's out "key : 0"
What am I doing wrong here?
Also, I would like to add the value.
So the our put would be:
key : fr , value : frederick
It Can be done in old bash versions.
In older versions of bash you can use the whole environment variable set to implement associative arrays (also called hashes)
export HASH_PREFIX="I_AM_A_HASH"
hash-set() {
HASH_NAME="$1" ; shift
HASH_KEY="$1" ; shift
HASH_VAL="$1" ; shift
eval "export ${HASH_PREFIX}_${HASH_NAME}_KEY_${HASH_KEY}='$HASH_VAL'"
}
hash-get() {
HASH_NAME="$1" ; shift
HASH_KEY="$1" ; shift
eval "echo \"\$${HASH_PREFIX}_${HASH_NAME}_KEY_${HASH_KEY}\""
}
hash-keys() {
HASH_NAME="$1" ; shift
HASH_PREFIX_NAME_LENGTH=$(( ${#HASH_PREFIX} + ${#HASH_NAME} + 6 ))
declare -x | while read -r LINE_READ ; do
LINE_READ="${LINE_READ:11}"
if [ x"${LINE_READ:0:HASH_PREFIX_NAME_LENGTH}" \
= x"${HASH_PREFIX}_${HASH_NAME}_KEY_" \
]
then
LINE_READ="${LINE_READ:HASH_PREFIX_NAME_LENGTH}"
LINE_READ="${LINE_READ/=*/}"
echo "${LINE_READ}"
fi
done
}
hash-set sites "fr" "frederick"
hash-set sites "an" "annapolis"
for i in $(hash-keys sites) ; do
echo "key: $i, value: $(hash-get sites $i)"
done
The keys are restricted to the same characters as Environment Variables (0-9,a-z,A-Z,_).
You could workaround by using "_xx" to mean non-alphanumeric ascii values (example "_5f" for "_" and "_3f" for "?"). rosettacode has how to convert back and forth between ascii characters and hex in pure-bash.
Also on mac laptop you can install homebrew then use it to install a newer bash.
You could also use the full associative arrays in awk or perl or ruby or python.
The problem is declare -a.
As per the man page, it should be declare -A.
declare [-aAfFgilrtux] [-p] [name[=value] ...]
...
-a Each name is an indexed array variable (see Arrays above).
-A Each name is an associative array variable (see Arrays above).
Try this instead:
declare -A sites
sites=(["fr"]="frederick" ["an"]="annapolis")
for i in "${!sites[#]}"
do
echo "key: $i, value: ${sites[$i]}"
done
I think it maybe a lack of capitalization getting in the way...
declare -A _sites=( ["fr"]="frederick" ["an"]="annapolis" )
for i in "${!_sites[#]}"; do
printf '%s -> %s\n' "${i}" "${_sites[$i]}"
done
Resources that where helpful in sorting the above out are not limited to the following;
An answer from #anubhava regarding declare -A usage
An answer from #lhunath that also covers a work around for Bash versions 3 or lower.
After reading the other posted answer's comments I think you'll want that last listed answer's work-around if stuck with older version of Bash that cannot be updated for reasons.
#Fenn a few more notes...
hash-set() {
HASH_NAME="$1" ; shift
HASH_KEY="$1" ; shift
HASH_VAL="$1" ; shift
eval "export ${HASH_PREFIX}_${HASH_NAME}_KEY_${HASH_KEY}='$HASH_VAL'"
}
... without shifting, or eval, and with required arguments might look like...
hash_set(){
local _name="${1:?${FUNCNAME[0]} not provided a Hash Name}"
local _key="${2:?${FUNCNAME[0]} not provided a Hash Key}"
local _value="${3:?${FUNCNAME[0]} not provided a Value}"
declare -g "${HASH_PREFIX}_${_name}_KEY_${_key}='${_value}'"
}
... hopefully this is a bit more helpful in translating that answer into something that can be up-voted.

Resources