Pair two arrays into one associative array - arrays

I have two arrays:
SUBJECT_IDS=(44456 11123 77789)
DCM_FILES=("./77789/77789/"DICOM"" "./11123/11123/"DICOM"" "./44456/44456/77789/"DICOM"" )
Which I then sorted to make the indexing easier/already sort of 'pair' it by index:
IFS=$'\n'
sorted_SUBJECT_IDS=($(sort <<<"${SUBJECT_IDS[*]}"))
sorted_DCM_FILES=($(sort <<<"${DCM_FILES[*]}"))
How would I create an associative array by pairing the two arrays by indice?
headers=(
[11123]=./11123/11123/DICOM
[44456]=./44456/44456/DICOM
[77789]=./77789/77789/DICOM
)
Here are my attempts at creating an associative array:
Attempt 1:
declare -A headers
i=0
for i in "${sorted_SUBJECT_IDS[#]}"
do
headers["$i"]="${sorted_DCM_FILES[i]}";
echo "$i" : "${sorted_DCM_FILES[i]}"
((i++))
done
Output:
11123 :
44456 :
77789 :
Attempt 2:
typeset -A hash
hash=("$(#){sorted_SUBJECT_IDS:^sorted_DCM_FILES}")
Output:
bash: hash: "$(#){sorted_SUBJECT_IDS:^sorted_DCM_FILES}": mustuse subscript when assigning associative array

Your sort is not working the way you want it to be and moreover your array iteration should be by the index not by items.
You may use:
sorted_SUBJECT_IDS=($(sort -n <(printf '%s\n' "${SUBJECT_IDS[#]}")))
sorted_DCM_FILES=($(sort -n <(printf '%s\n' "${DCM_FILES[#]}")))
declare -A headers
for i in "${!sorted_SUBJECT_IDS[#]}"; do
headers["${sorted_SUBJECT_IDS[i]}"]="${sorted_DCM_FILES[i]}"
done
# check header associative array
declare -p headers
declare -A headers=([77789]="./77789/77789/DICOM" [44456]="./44456/44456/77789/DICOM" [11123]="./11123/11123/DICOM" )

Related

How to create a associative array in a range, filled by given value?

How to create an associative array in a range, every position filled by the same given value ?
What's known or given:
Creating an associative array:
declare -A my_array
define range of array:
ug=-10
og=10
Value for filling:
filling_value=0
filling_value=0
ug=-10
og=10
unset my_array
declare -A my_array
for ((i=ug; i<=og; i++))
do
my_array[${i}]="${filling_value}"
done
Results:
$ typeset -p my_array
declare -A my_array=([10]="0" [-10]="0" [0]="0" [1]="0" [2]="0" [3]="0" [4]="0" [5]="0" [6]="0" [7]="0" [8]="0" [9]="0" [-7]="0" [-6]="0" [-5]="0" [-4]="0" [-3]="0" [-2]="0" [-1]="0" [-9]="0" [-8]="0" )
A slight variation that generates the same array:
for ((i=ug; i<=og; i++))
do
my_array+=( [${i}]="${filling_value}" )
done

How to re-sort an indexed array of associative arrays, based on the values in the associative arrays?

In my shell code, I have an indexed array, containing the names of associative arrays:
declare -A assoc1=([name]=aaa [age]=20)
declare -A assoc2=([name]=bbb [age]=40)
declare -A assoc3=([name]=ccc [age]=25)
indexed_array=(assoc1 assoc2 assoc3)
So, using the above, ${indexed_array[#]} equals assoc1 assoc2 assoc3.
I want a sort_array function that can re-sort the values in indexed_array, so that the associative array with the highest age (assoc2) is listed first or last, like so:
new_indexed_array=( $(echo ${indexed_array[#]} | sort_by 'age' 'desc') )
After that I should get re-ordered contents in the new array:
declare -p new_indexed_array
# gives "assoc2 assoc3 assoc1"
I have some boilerplate code to get into the array values, but wasn't able to get any further in sorting the arrays..
function sort_by {
# for each hash in the given array
get_stdin # (custom func, sets $STDIN)
for hash in ${STDIN[#]}
do
# get the hash keys
hash_keys="$(eval "echo \${!$hash[#]}")"
# for each key
for hashkey in $hash_keys
do
# reset
return_the_array=false
# if $hashkey matches the key given
if [ "$hashkey" = "$1" ];then
# check the value of this one if highest/lowest
# (compared to previous ones)
# and then return if yes/mo (asc/desc)
fi
# if $return_the_array = true, then we found the right key and
# it's higher/lower
if [ "$return_the_array" = true ];then
# do stuff
fi
done
done
}
If you have Bash 4.3 or newer, you can use namerefs for this, as follows:
sort_by() {
local arr field sort_params elem
declare -n arr=$1
field=$2
# Build array with sort parameters
[[ $3 == 'desc' ]] && sort_params+=('-r')
[[ $field == 'age' ]] && sort_params+=('-n')
# Schwartzian transform
for elem in "${arr[#]}"; do
declare -n ref=$elem
printf '%s\t%s\n' "${ref["$field"]}" "$elem"
done | sort "${sort_params[#]}" | cut -f2
}
declare -A assoc1=([name]=aaa [age]=20)
declare -A assoc2=([name]=bbb [age]=40)
declare -A assoc3=([name]=ccc [age]=25)
indexed_array=(assoc1 assoc2 assoc3)
readarray -t byage < <(sort_by indexed_array age desc)
declare -p byage
readarray -t byname < <(sort_by indexed_array name asc)
declare -p byname
The calling syntax is a bit different:
sort_by ARRAYNAME FIELD SORTORDER
and the output is one element per line, so to read it back into an array, we have to use something like readarray (see examples at the end).
First, we use a nameref to assign the arrayname to arr:
declare -n arr=$1
arr now behaves as if it were the actual array.
Then, we build an array with the parameters for sort: if the third parameter is desc, we use -r, and if the field is age, we use -n. This could be made a bit smarter and check if the field contains numeric values or not, and set -n accordingly.
We then iterate over the elements of arr, the elements of which are the names of the associative arrays. In the loop, we assign the names to ref:
declare -n ref=$elem
ref now behaves like the actual associative array.
To sort, we use a Schwartzian transform (decorate – sort – undecorate) by printing lines with the chosen field name and then the array name; for example, for age, we'd get
20 assoc1
40 assoc2
25 assoc3
This is piped to sort with the proper parameters, and with cut -f2 we remove the sort field again.
The output for the examples looks like this:
declare -a byage=([0]="assoc2" [1]="assoc3" [2]="assoc1")
declare -a byname=([0]="assoc1" [1]="assoc2" [2]="assoc3")
Notice that declare -n declares local parameters in the function, so they don't pollute the global namespace.
Just for reference, I am using a modified version of the accepted answer:
function sort_by {
local field sort_params elem
field=$1
# Build array with sort parameters
[[ $2 == 'desc' ]] && sort_params+=('-r')
[[ $field == 'age' ]] && sort_params+=('-n')
# Schwartzian transform,
# get piped array contents from $(cat)
while read -r elem; do
declare -n ref=$elem
printf '%s\t%s\n' "${ref["$field"]}" "$elem"
done | sort "${sort_params[#]}" | cut -f2 | tr '\n' ' '
}
The difference between this function and the accepted answer, is that this function above expects to receive the contents of the indexed array as piped input, rather than taking the array name as the first parameter..
Therefore, my function can be called like so:
echo ${someIndexedArray[#]} | sort_by 'age' 'desc'
(And this func outputs everything on the same line, as opposed to newlines)
The benefit of this (for me) is that it now works in the CMS I am using, and also ,the sort_by function does not need to know the name of the array - which would not be passed by the CMS.

How can I see which values exists in Bash array?

Given an array (in Bash), is there a command that prints the contents of the array according to indices?
Something like that: arr[0]=... , arr[1]=... ,...
I know that I can print it in a for loop, but I am looking for a command that does it.
Given an array with contiguous indices starting at 0:
$ arr=(one two three)
And non-contiguous indices:
$ declare -a arr2='([0]="one" [2]="two" [5]="three")'
You can print the values:
$ echo ${arr[*]} # same with arr2
one two three
Or, use a C style loop for arr:
$ for (( i=0;i<${#arr[#]};i++ )); do echo "arr[$i]=${arr[$i]}"; done
arr[0]=one
arr[1]=two
arr[2]=three
But that won't work for arr2.
So, you can expand the indices (contiguous or not) and print index, value like so:
$ for i in "${!arr2[#]}"; do echo "arr2[$i]=${arr2[$i]}"; done
arr2[0]=one
arr2[2]=two
arr2[5]=three
Or inspect it with declare -p:
$ declare -p arr
declare -a arr='([0]="one" [1]="two" [2]="three")'
Which also works if the array has non-contiguous indices (where the C loop would break):
$ declare -p arr2
declare -a arr2='([0]="one" [2]="two" [5]="three")'
Note: A common mistake is to use the sigil $ and thinking you are addressing that array or named value. It is the unadorned name that is used since the sigil will dereference and tell details of the name contained instead of that name:
$ k=arr
$ declare -p $k
declare -a arr='([0]="one" [1]="two" [2]="three")' # note this is 'arr' , not 'k'
$ declare -p k
declare -- k="arr"
Since declare with no arguments will print the entire Bash environment at that moment, you can also use utilities such as sed grep or awk against that output:
$ declare | grep 'arr'
arr=([0]="one" [1]="two" [2]="three")
k=arr

Using Bash, is it possible to store an array in a dictionary

With bash, it is possible to store an array in a dictionary? I have shown some sample code of fetching an array from the dictionary but it seems to lose the fact that it is an array.
I expect it is the dict+=(["pos"]="${array[#]}") command but am unsure of how to do this or if it is even possible.
# Normal array behaviour (just an example)
array=(1 2 3)
for a in "${array[#]}"
do
echo "$a"
done
# Outputs:
# 1
# 2
# 3
# Array in a dictionary
declare -A dict
dict+=(["pos"]="${array[#]}")
# When I fetch the array, it is not an array anymore
posarray=("${dict[pos]}")
for a in "${posarray[#]}"
do
echo "$a"
done
# Outputs:
# 1 2 3
# but I want
# 1
# 2
# 3
No, but there are workarounds.
Using printf '%q ' + eval
You can flatten your array into a string:
printf -v array_str '%q ' "${array[#]}"
dict["pos"]=$array_str
...and then use eval to expand that array back:
# WARNING: Only safe if array was populated with eval-safe strings, as from printf %q
key=pos; dest=array
printf -v array_cmd "%q=( %s )" "$dest" "${dict[$key]}"
eval "$array_cmd"
Note that this is only safe if your associative array is populated through the code using printf '%q ' to escape the values before they're added; content that avoids this process is potentially unsafe to eval.
Using base64 encoding
Slower but safer (if you can't prevent modification of your dictionary's contents by untrusted code), another approach is to store a base64-encoded NUL-delimited list:
dict["pos"]=$(printf '%s\0' "${array[#]}" | openssl enc base64)
...and read it out the same way:
array=( )
while IFS= read -r -d '' item; do
array+=( "$item" )
done < <(openssl enc -d base64 <<<"${dict["pos"]}"
Using Multiple Variables + Indirect Expansion
This one's actually symmetric, though it requires bash 4.3 or newer. That said, it restricts your key names to those which are permissible as shell variable names.
key=pos
array=( "first value" "second value" )
printf -v var_name 'dict_%q' "$key"
declare -n var="$var_name"
var=( "${array[#]}" )
unset -n var
...whereafter declare -p dict_pos will emit declare -a dict_pos=([0]="first value" [1]="second value"). On the other end, for retrieval:
key=pos
printf -v var_name 'dict_%q' "$key"
declare -n var="$var_name"
array=( "${var[#]}" )
unset -n var
...whereafter declare -p array will emit declare -a array=([0]="first value" [1]="second value").
Dictionaries are associative arrays, so the question rephrased is: "Is it possible to store an array inside another array?"
No, it's not. Arrays cannot be nested.
dict+=(["pos"]="${array[#]}")
For this to work you'd need an extra set of parentheses to capture the value as an array and not a string:
dict+=(["pos"]=("${array[#]}"))
But that's not legal syntax.

How to pass an associative array as argument to a function in Bash?

How do you pass an associative array as an argument to a function? Is this possible in Bash?
The code below is not working as expected:
function iterateArray
{
local ADATA="${#}" # associative array
for key in "${!ADATA[#]}"
do
echo "key - ${key}"
echo "value: ${ADATA[$key]}"
done
}
Passing associative arrays to a function like normal arrays does not work:
iterateArray "$A_DATA"
or
iterateArray "$A_DATA[#]"
If you're using Bash 4.3 or newer, the cleanest way is to pass the associative array by name reference and then access it inside your function using a name reference with local -n. For example:
function foo {
local -n data_ref=$1
echo ${data_ref[a]} ${data_ref[b]}
}
declare -A data
data[a]="Fred Flintstone"
data[b]="Barney Rubble"
foo data
You don't have to use the _ref suffix; that's just what I picked here. You can call the reference anything you want so long as it's different from the original variable name (otherwise youll get a "circular name reference" error).
I had exactly the same problem last week and thought about it for quite a while.
It seems, that associative arrays can't be serialized or copied. There's a good Bash FAQ entry to associative arrays which explains them in detail. The last section gave me the following idea which works for me:
function print_array {
# eval string into a new associative array
eval "declare -A func_assoc_array="${1#*=}
# proof that array was successfully created
declare -p func_assoc_array
}
# declare an associative array
declare -A assoc_array=(["key1"]="value1" ["key2"]="value2")
# show associative array definition
declare -p assoc_array
# pass associative array in string form to function
print_array "$(declare -p assoc_array)"
Based on
Florian Feldhaus's solution:
# Bash 4+ only
function printAssocArray # ( assocArrayName )
{
var=$(declare -p "$1")
eval "declare -A _arr="${var#*=}
for k in "${!_arr[#]}"; do
echo "$k: ${_arr[$k]}"
done
}
declare -A conf
conf[pou]=789
conf[mail]="ab\npo"
conf[doo]=456
printAssocArray "conf"
The output will be:
doo: 456
pou: 789
mail: ab\npo
Update, to fully answer the question, here is an small section from my library:
Iterating an associative array by reference
shopt -s expand_aliases
alias array.getbyref='e="$( declare -p ${1} )"; eval "declare -A E=${e#*=}"'
alias array.foreach='array.keys ${1}; for key in "${KEYS[#]}"'
function array.print {
array.getbyref
array.foreach
do
echo "$key: ${E[$key]}"
done
}
function array.keys {
array.getbyref
KEYS=(${!E[#]})
}
# Example usage:
declare -A A=([one]=1 [two]=2 [three]=3)
array.print A
This we a devlopment of my earlier work, which I will leave below.
#ffeldhaus - nice response, I took it and ran with it:
t()
{
e="$( declare -p $1 )"
eval "declare -A E=${e#*=}"
declare -p E
}
declare -A A='([a]="1" [b]="2" [c]="3" )'
echo -n original declaration:; declare -p A
echo -n running function tst:
t A
# Output:
# original declaration:declare -A A='([a]="1" [b]="2" [c]="3" )'
# running function tst:declare -A E='([a]="1" [b]="2" [c]="3" )'
You can only pass associative arrays by name.
It's better (more efficient) to pass regular arrays by name also.
Here is a solution I came up with today using eval echo ... to do the indirection:
print_assoc_array() {
local arr_keys="\${!$1[#]}" # \$ means we only substitute the $1
local arr_val="\${$1[\"\$k\"]}"
for k in $(eval echo $arr_keys); do #use eval echo to do the next substitution
printf "%s: %s\n" "$k" "$(eval echo $arr_val)"
done
}
declare -A my_arr
my_arr[abc]="123"
my_arr[def]="456"
print_assoc_array my_arr
Outputs on bash 4.3:
def: 456
abc: 123
yo:
#!/bin/bash
declare -A dict
dict=(
[ke]="va"
[ys]="lu"
[ye]="es"
)
fun() {
for i in $#; do
echo $i
done
}
fun ${dict[#]} # || ${dict[key]} || ${!dict[#] || ${dict[$1]}
eZ
Here's another way: you can manually serialize the associative array as you pass it to a function, then deserialize it back into a new associative array inside the function:
1. Manual passing (via serialization/deserialization) of the associative array
Here's a full, runnable example from my eRCaGuy_hello_world repo:
array_pass_as_bash_parameter_2_associative.sh:
# Print an associative array using manual serialization/deserialization
# Usage:
# # General form:
# print_associative_array array_length array_keys array_values
#
# # Example:
# # length indices (keys) values
# print_associative_array "${#array1[#]}" "${!array1[#]}" "${array1[#]}"
print_associative_array() {
i=1
# read 1st argument, the array length
array_len="${#:$i:1}"
((i++))
# read all key:value pairs into a new associative array
declare -A array
for (( i_key="$i"; i_key<$(($i + "$array_len")); i_key++ )); do
i_value=$(($i_key + $array_len))
key="${#:$i_key:1}"
value="${#:$i_value:1}"
array["$key"]="$value"
done
# print the array by iterating through all of the keys now
for key in "${!array[#]}"; do
value="${array["$key"]}"
echo " $key: $value"
done
}
# Let's create and load up an associative array and print it
declare -A array1
array1["a"]="cat"
array1["b"]="dog"
array1["c"]="mouse"
# length indices (keys) values
print_associative_array "${#array1[#]}" "${!array1[#]}" "${array1[#]}"
Sample output:
a: cat
b: dog
c: mouse
Explanation:
For a given function named print_associative_array, here is the general form:
# general form
print_associative_array array_length array_keys array_values
For an array named array1, here is how to obtain the array length, indices (keys), and values:
array length: "${#array1[#]}"
all of the array indices (keys in this case, since it's an associative array): "${!array1[#]}"
all of the array values: "${array1[#]}"
So, an example call to print_associative_array would look like this:
# example call
# length indices (keys) values
print_associative_array "${#array1[#]}" "${!array1[#]}" "${array1[#]}"
Putting the length of the array first is essential, as it allows us to parse the incoming serialized array as it arrives into the print_associative_array function inside the magic # array of all incoming arguments.
To parse the # array, we'll use array slicing, which is described as follows (this snippet is copy-pasted from my answer here):
# array slicing basic format 1: grab a certain length starting at a certain
# index
echo "${#:2:5}"
# │ │
# │ └────> slice length
# └──────> slice starting index (zero-based)
2. [Better technique than above!] Pass the array by reference
...as #Todd Lehman explains in his answer here
# Print an associative array by passing the array by reference
# Usage:
# # General form:
# print_associative_array2 array
# # Example
# print_associative_array2 array1
print_associative_array2() {
# declare a local **reference variable** (hence `-n`) named `array_reference`
# which is a reference to the value stored in the first parameter
# passed in
local -n array_reference="$1"
# print the array by iterating through all of the keys now
for key in "${!array_reference[#]}"; do
value="${array_reference["$key"]}"
echo " $key: $value"
done
}
echo 'print_associative_array2 array1'
print_associative_array2 array1
echo ""
echo "OR (same thing--quotes don't matter in this case):"
echo 'print_associative_array2 "array1"'
print_associative_array2 "array1"
Sample output:
print_associative_array2 array1
a: cat
b: dog
c: mouse
OR (same thing--quotes don't matter in this case):
print_associative_array2 "array1"
a: cat
b: dog
c: mouse
See also:
[my answer] a more-extensive demo of me serializing/deserializing a regular "indexed" bash array in order to pass one or more of them as parameters to a function: Passing arrays as parameters in bash
[my answer] a demo of me passing a regular "indexed" bash array by reference: Passing arrays as parameters in bash
[my answer] array slicing: Unix & Linux: Bash: slice of positional parameters
[my question] Why do the man bash pages state the declare and local -n attribute "cannot be applied to array variables", and yet it can?
Excellent. The simple solution described by #Todd Lehman, solved my associative array passing problem. I had to pass 3 parameters, an integer, an associative array and an indexed array to a function.
A while ago I'd read that since arrays in bash are not first-class entities, arrays could not be passed as arguments to functions. Clearly that's not the whole truth after all. I've just implemented a solution where a function handles those parameters, something like this...
function serve_quiz_question() {
local index="$1"; shift
local -n answers_ref=$1; shift
local questions=( "$#" )
current_question="${questions[$index]}"
echo "current_question: $current_question"
#...
current_answer="${answers_ref[$current_question]}"
echo "current_answer: $current_answer"
}
declare -A answers
answers[braveheart]="scotland"
answers[mr robot]="new york"
answers[tron]="vancouver"
answers[devs]="california"
# integers would actually be assigned to index \
# by iterating over a random sequence, not shown here.
index=2
declare -a questions=( "braveheart" "devs" "mr robot" "tron" )
serve_quiz_question "$index" answers "${questions[#]}"
As the local variables get assigned, I had to shift the positional parameters away, in order to end with the ( "$#" ) assigning what's left to the indexed questions array.
The indexed array is needed so that we can reliably iterate over all the questions, either in random or ordered sequence. Associative arrays are not ordered data structures, so are not meant for any sort of predictable iteration.
Output:
current_question: mr robot
current_answer: new york
From the best Bash guide ever:
declare -A fullNames
fullNames=( ["lhunath"]="Maarten Billemont" ["greycat"]="Greg Wooledge" )
for user in "${!fullNames[#]}"
do
echo "User: $user, full name: ${fullNames[$user]}."
done
I think the issue in your case is that $# is not an associative array: "#: Expands to all the words of all the positional parameters. If double quoted, it expands to a list of all the positional parameters as individual words."

Resources