SOKS
SOKS

Reputation: 190

Iterate over lists embedded as values in key/value pairs in bash

I'm trying to get a (key,multiple-value) structure (some sort of hashmap) in bash, like this :

[   
  [ "abc" : 1, 2, 3, 4 ],
  [ "def" : "w", 33, 2 ]    
]

I'd like to iterate through eack key (some kind of for key in ..., and get each value with something like map["def",2] or map[$key,2].

I've seen a couple of threads talking about single-value hashmap, but nothing about this issue.

I could go with N arrays, N being the amount of key in my map, filled with every field in a row, but I don't want to duplicate code as much as possible.

Thanks in advance !

Edit : I'd like to go through the structure with something like this :

for key in ${map[@]} do;
  echo $key # "abc" then "def"
  for value in ${map[$key,@]} do;
    ...
  done
done

Upvotes: 0

Views: 1781

Answers (2)

Charles Duffy
Charles Duffy

Reputation: 295510

Using modern bash features with the multiple-array case:


Assignment (manual):

map_abc=( 1 2 3 4 )
map_def=( w 33 2 )

Assignment (programmatic):

append() {
  local array_name="${1}_$2"; shift; shift
  declare -g -a "$array_name"
  declare -n array="$array_name" # BASH 4.3 FEATURE
  array+=( "$@" )
}
append map abc 1 2 3 4
append map def w 33 2

Iteration (done inside a function to contain the namevar's scope):

iter() {
  for array in ${!map_@}; do
    echo "Iterating over array ${array#map_}"
    declare -n cur_array="$array" # BASH 4.3 FEATURE
    for key in "${!cur_array[@]}"; do
      echo "$key: ${cur_array[$key]}"
    done
  done
}
iter

This can also be done without namevars, but in an uglier and more error-prone fashion. (To be clear, I believe the code given here uses eval safely, but it's easy to get wrong -- if trying to build your own implementation on this template, please be very cautious).

# Compatible with older bash (should be through 3.x).
append() {
  local array_name="${1}_$2"; shift; shift
  declare -g -a "$array_name"

  local args_str cmd_str
  printf -v args_str '%q ' "$@"
  printf -v cmd_str "%q+=( %s )" "$array_name" "$args_str"
  eval "$cmd_str"
}

...and, to iterate in a way compatible with bash back through 3.x:

for array in ${!map_@}; do
  echo "Iterating over array ${array#map_}"

  printf -v cur_array_cmd 'cur_array=( ${%q[@]} )' "$array"
  eval "$cur_array_cmd"

  for key in "${!cur_array[@]}"; do
    echo "$key: ${cur_array[$key]}"
  done
done

This is more computationally efficient than filtering through a single large array (the other answer given) -- and, when namevars are available, arguably results in cleaner code as well.

Upvotes: 3

glenn jackman
glenn jackman

Reputation: 246877

Do-able. The declaration is somewhat ugly

declare -A map=(
    [abc,0]=1
    [abc,1]=2
    [abc,2]=3
    [abc,3]=4
    [def,0]=w
    [def,1]=33
    [def,2]=2
)
key="def"
i=1
echo "${map[$key,$i]}"   # => 33

Iterating: helpful to keep a separate array of "keys":

keys=(abc def)

Then

for key in "${keys[@]}"; do 
    echo "$key"
    for idx in "${!map[@]}"; do 
        if [[ $idx == $key,* ]]; then 
            n=${idx##*,}
            printf "\t%s\t%s\n" "$n" "${map["$idx"]}"
        fi
    done
done
abc
    0   1
    1   2
    2   3
    3   4
def
    1   33
    0   w
    2   2

Upvotes: 1

Related Questions