Two-Way Hash in Bash? - 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
...

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

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

Evaluate variable in if statement

So I have an array like:
al_ap_version=('ap_version' '[[ $data -ne $version ]]')
And the condition gets evaluated inside a loop like:
for alert in alert_list; do
data=$(tail -1 somefile)
condition=$(eval echo \${$alert[1]})
if eval "$condition" ; then
echo SomeAlert
fi
done
Whilst this generally works with many scenarios, if $data returns something like "-/-" or "4.2.9", I get errors as it doesn't seem to like complex strings in the variable.
Obviously I can't enclose the variable in single quotes as it won't expand so I'm after any ideas to expand the $data variable (or indeed the $version var which suffers the same possible fate) in a way that the evaluation can handle?
Ignoring the fact that eval is probably super dangerous to use here (unless the data in somefile is controlled by you and only you), there are a few issues to fix in your example code.
In your for loop, alert_list needs to be $alert_list.
Also, as pointed out by #choroba, you should be using != instead of -ne since your input isn't always an integer.
Finally, while debugging, you can add set -x to the top of your script, or add -x to the end of your shebang line to enable verbose output (helps to determine how bash is expanding your variables).
This works for me:
#!/bin/bash -x
data=2.2
version=1
al_ap_version=('ap_version' '[[ $data != $version ]]')
alert_list='al_ap_version'
for alert in $alert_list; do
condition=$(eval echo \${$alert[1]})
if eval "$condition"; then
echo "alert"
fi
done
You could try a more functional approach, even though bash is only just barely capable of such things. On the whole, it is usually a lot easier to pack an action to be executed into a bash function and refer to it with the name of the function, than to try to maintain the action as a string to be evaluated.
But first, the use of an array of names of arrays is awkward. Let's get rid of it.
It's not clear to me the point of element 0, ap_version, in the array al_ap_version but I suppose it has something to do with error messages. If the order of alert processing isn't important, you could replace the list of names of arrays with a single associative array:
declare -A alert_list
alert_list[ap_version]=... # see below
alert_list[os_dsk]=...
and then process them with:
for alert_name in ${!alert_list[#]}; do
alert=${alert_list[$alert_name]}
...
done
Having done that, we can get rid of the eval, with its consequent ugly necessity for juggling quotes, by creating a bash function for each alert:
check_ap_version() {
(($version != $1))
}
Edit: It seems that $1 is not necessarily numeric, so it would be better to use a non-numeric comparison, although exact version match might not be what you're after either. So perhaps it would be better to use:
check_ap_version() {
[[ $version != $1 ]]
}
Note the convention that the first argument of the function is the data value.
Now we can insert the name of the function into the alert array, and call it indirectly in the loop:
declare -A alert_list
alert_list[ap_version]=check_ap_version
alert_list[os_dsk]=check_op_dsk
check_alerts() {
local alert_name alert
local data=$(tail -1 somefile)
for alert_name in ${!alert_list[#]}; do
alert=${alert_list[$alert_name]}
if $alert "$data"; then
signal_alert $alert_name
fi
done
}
If you're prepared to be more disciplined about the function names, you can avoid the associative array, and thereby process the alerts in order. Suppose, for example, that every function has the name check_<alert_name>. Then the above could be:
alert_list=(ap_version os_dsk)
check_alerts() {
local alert_name
local data=$(tail -1 somefile)
for alert_name in $alert_list[#]; do
if check_$alert_name "$data"; then
signal_alert $alert_name
fi
done
}

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