How to create a map of key:array in shell?

27,653

Solution 1

Bash does not support multi-dimensional arrays, but I don't think you need one. You can store a string in the form of a list in an array element, which will give you what you ask for.

# My made-up version of getServices
getServices() {
    nm="$1"
    last=${nm##*Type}
    retn=(${last}1 ${last}2 ${last}3 ${last}4)
    echo "${retn[@]}"
}


declare -A serviceList
listService(){
    serviceType="$1"

    # Here I use the key to make an assignment, which adds to the hash
    serviceList["$serviceType"]=$(getServices $serviceType) 
}

listService serviceTypeA
listService serviceTypeB
listService serviceTypeC

for key in ${!serviceList[@]}
do
    echo "\"$key\": ${serviceList[$key]}"
done

Gives:

"serviceTypeC": C1 C2 C3 C4
"serviceTypeB": B1 B2 B3 B4
"serviceTypeA": A1 A2 A3 A4

EDIT for new question:

alter:

arrayMap["$param"]=$values     # THIS IS THE KEY LINE
valuesList=${arrayMap[$param]} 

to:

arrayMap["$param"]=${values[@]} 
valuesList=( ${arrayMap[$param]}  )  

When you refer to an array variable by just it's name ($values) you only get the first element.

Solution 2

As cdarke already mentioned, bash arrays are one-dimensional. Over the years, folks have come up with ways to "fake" multi-dimensional arrays.

Two methods I've used are to maintain an array of array descriptions, or an array of pointers to other arrays. I'll answer with the former; the latter should be obvious if you want to explore on your own.

Here's a minimal example of array content getting used to populate variables:

#!/usr/bin/env bash

declare -A a=(
  [b]='([0]="one" [1]="two")'
  [c]='([0]="three" [1]="four")'
)

declare -p a

for key in ${!a[@]}; do
  declare -a $key="${a[$key]}"
  declare -p $key
done

Produces:

declare -A a=([b]="([0]=\"one\" [1]=\"two\")" [c]="([0]=\"three\" [1]=\"four\")" )
declare -a b=([0]="one" [1]="two")
declare -a c=([0]="three" [1]="four")

The critical bit here is that you're using declare to refer to the value of $key, since you can't just say $var="value" in bash.

Of course, you don't need to name your variables for the value of $key if you don't want to. Storing values in, say $value, would free you up to use special characters in $key.

An even simpler alternative, if it doesn't offend your sensibilities or restrict your key names too much, is to store the entire output of a declare -p command in the value of the array, and then eval it when you need it. For example:

declare -A a=(
 [b]='declare -a b=([0]="one" [1]="two")'
 [c]='declare -a c=([0]="three" [1]="four")'
)

for key in ${!a[@]}; do
  eval "${a[$key]}"
done

Some people don't like eval. :-) It remains, however in your toolbox.

In your case, it's a little hard to advise because you haven't provided a full MCVE, but here's my contrived example.

#!/usr/bin/env bash

# contrived getServices function, since you didn't provide one
getServices() {
    local -a value=()
    local last="${1:$((${#1}-1)):1}"   # last character of $1
    for n in $( seq 1 $(( $RANDOM / 8192 + 1 )) ); do
      value+=(${last}${n})
    done
    declare -p value     # output of this function is actual bash code.
}

# populate the array
listService() {
    servicesList[$1]=$( getServices $1 )
}

# Initialize this as empty to make `eval` safer
declare -A servicesList=()

# These services seem interesting.
listService serviceA
listService serviceB
listService serviceC

# Note that we're stepping through KEYS here, not values.
for row in "${!servicesList[@]}"; do
    printf '"%s": ' "$row"
    eval "${servicesList[$row]}"   # Someone is bound to complain about this.
    for column in "${!value[@]}"; do
        # Add whatever $row and $column specific code you like here.
        printf '%s ' "${value[$column]}"
    done
    printf "\n"
done

My output:

$ bash 2dimarrayexample
"serviceC": C1
"serviceB": B1 B2 B3 B4
"serviceA": A1 A2

Of course, your output may differ, since getServices produces random output. :)

Share:
27,653
saurav
Author by

saurav

Updated on January 17, 2020

Comments

  • saurav
    saurav over 4 years

    I want to create map in shell. Where each value is an array. So the map is key:array pair. For example it can be like this :

    "Key1" : a1 a2 a3 a4
    "key2" : b1 b2 b3
    "key3" : c1
    

    basically my code looks like this

    listService(){
    serviceType=$1
    servicesList=($(getServices $serviceType))
    }
    
    listService serviceTypeA
    listService serviceTypeB
    listService serviceTypeC
    

    here getServices is a function which returns an array of services based on the argument passed as $serviceType. So every time i call the listService function my serviceList gets overridden by new service list. But I want to keep all the services from different service type in form of a map like this :

    "serviceA" : a1 a2 a3 a4
    "serviceB" : b1 b2 b3
    "serviceC" : c1
    

    After that I want to access each array based on the key. How to achieve this.

    Thanks in advance for your help.

    Edit : I tried the answer provided by @cdarke . Here is my code now :

    #!/bin/bash
    declare -A arrayMap
    
    getValues(){
      key=$1
      case $key in
        AAA )
        arr=( AA AAA AAAA )
          ;;
        BBB )
        arr=( BB BB BBBB )
          ;;
        CCC )
        arr=()
        ;;
        esac
        echo "${arr[@]}"
    }
    
    fillArrayMap(){
      param=$1
      values=( $(getValues $param) )
      printf "\nIn $param\n"
      echo -e "\nArray values is: ${values[@]}\n"
      printf "\nLength of the array values is : ${#values[@]}\n"
      arrayMap["$param"]=$values #THIS IS THE KEY LINE
      valuesList=${arrayMap[$param]} 
      echo -e "\nArray valuesList is: ${valuesList[@]}\n"
      printf "\nLength of the array valuesList is : ${#valuesList[@]}\n"
    }
    
    fillArrayMap AAA
    fillArrayMap BBB
    fillArrayMap CCC
    

    Now from output I can see valuesList is getting only the first element of the values array. But I want valuesList to contain all the elements returned by the method getValues. i.e

    valuesList= ${arrayMap[$param]}
    

    now valuesList should contain all the elements, instead now it contains only 1 element. How to fix that ?

    Note: My goal is to access each individual element like AAA or AA, I don't need it as a whole as a string like AA AAA AAAA