Accessing bash array keys (mac) - macos

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.

Related

Easier way for conditional string op?

Supposed i have a variable
A_LOSS="5%"
I need to append as part of larger command (tc) if it can evaluate ala.
if [ -n $A_LOSS ]
then A_LOSS="loss $A_LOSS"
fi
Since I have a lot of these cases it quickly becomes rather verbose, so is there a smarter way or should I maybe just make a function for that ?
Without a function, bash gives you parameter substitution:
${A_LOSS:+"loss $A_LOSS"}
However, it can be cumbersome if you have a lot of options.
On surface, the goal is to reduce the verbosity of a code that check multiple variables for a content, and then prepend a prefix ("loss " in this case) to any non-empty variable.
Possible to create a function for this task, that will take a variable name, and a prefix (assuming different prefixes for different variables). Using the 'reference' variable (declare -n) make the function take variable "by reference".
function add_prefix {
declare -n ref=$1
declare prefix=$2
[ -n "$ref" ] && ref="$prefix $ref"
}
add_prefix A_LOSS "Loss "
add_prefix A_GAIN "Gain "
...
As per comment from Charles Duffy: the 'reference' variable is relatively new feature to bash. It is available in 4.4 (Mint 19.2), but not in 4.1. On older version, possible to use indirect references. If running older version, the following substitution will work:
function add_prefix {
local ref=$1 prefix=$2
[ -n "${!ref}" ] && read "$ref" <<< "$prefix ${!ref}"
}
add_prefix A_LOSS "Loss "
add_prefix A_GAIN "Gain "

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

Dynamic variable created in function not available in future calls

I have a script that is (supposed to be) assigning a dynamic variable name (s1, s2, s3, ...) to a directory path:
savedir() {
declare -i n=1
sn=s$n
while test "${!sn}" != ""; do
n=$n+1
sn=s$n
done
declare $sn=$PWD
echo "SAVED ($sn): ${!sn}"
}
The idea is that the user is in a directory they'd like to recall later on and can save it to a shell variable by typing 'savedir'. It -does- in fact write out the echo statement successfully: if I'm in the directory /home/mrjones and type 'savedir', the script returns:
SAVED (s1): /home/mrjones
...and I can further type:
echo $sn
and the script returns:
s1
...but typing either...
> echo $s1
...or
echo ${!sn}
...both return nothing (empty strings). What I want, in case it's not obvious, is this:
echo $s1
/home/mrjones
Any help is greatly appreciated! [apologies for the formatting...]
To set a variable using a name stored in another variable I use printf -v, in this example:
printf -v "$sn" '%s' "$PWD"
declare here is creating a variable local to the function, which doesn't seem to be what you want. Quoting from help declare:
When used in a function, declare makes NAMEs local, as with the local
command. The -g option suppresses this behavior.
so you can either try the -g or with the printf
Use an array instead.
savedir() {
s+=("$PWD")
echo "SAVED (s[$((${#s[#]}-1))]): ${s[${#s[#]}-1]}"
}

Iterate over several associative arrays

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/

Iterating over variable name in bash script

I needed to run a script over a bunch of files, which paths were assigned to train1, train2, ... , train20, and I thought 'why not make it automatic with a bash script?'.
So I did something like:
train1=path/to/first/file
train2=path/to/second/file
...
train20=path/to/third/file
for i in {1..20}
do
python something.py train$i
done
which didn't work because train$i echoes train1's name, but not its value.
So I tried unsuccessfully things like $(train$i) or ${train$i} or ${!train$i}.
Does anyone know how to catch the correct value of these variables?
Use an array.
Bash does have variable indirection, so you can say
for varname in train{1..20}
do
python something.py "${!varname}"
done
The ! introduces the indirection, so "get the value of the variable named by the value of varname"
But use an array. You can make the definition very readable:
trains=(
path/to/first/file
path/to/second/file
...
path/to/third/file
)
Note that this array's first index is at position zero, so:
for ((i=0; i<${#trains[#]}; i++)); do
echo "train $i is ${trains[$i]}"
done
or
for idx in "${!trains[#]}"; do
echo "train $idx is ${trains[$idx]}"
done
You can use array:
train[1]=path/to/first/file
train[2]=path/to/second/file
...
train[20]=path/to/third/file
for i in {1..20}
do
python something.py ${train[$i]}
done
Or eval, but it awfull way:
train1=path/to/first/file
train2=path/to/second/file
...
train20=path/to/third/file
for i in {1..20}
do
eval "python something.py $train$i"
done

Resources