bashbash4

How can I update an associative array inside a function passing it by parameter?


I have the following code which reads all the fields of a Json file (the path being PRIVATE_REGISTRATION_FILE and stores them into an associative array (PRIVATE_FIELDS) which I query later in my code:

declare -A PRIVATE_FIELDS
for PRICING_FIELD in $(jq -c -r '.fields[]' "${PRIVATE_REGISTRATION_FILE}")
do
  FIELD_KEY=$(jq -r '.label' <<< "${PRICING_FIELD}")
  PRIVATE_FIELDS["${FIELD_KEY}"]=${PRICING_FIELD}
done

The problem is that I do this several times with several files, even though the logic is always the same.

Hence, I was thinking to extract this logic into a function but I'm having a hard time passing the map parameter to it.

This is what I attempted:

function update_array
{
    FILE_NAME=$1
    eval "declare -A MAP="${2#*=}
    for PRICING_FIELD in $(jq -c -r '.fields[]' "${FILE_NAME}")
    do
        FIELD_KEY=$(jq -r '.label' <<< "${PRICING_FIELD}")
        MAP["${FIELD_KEY}"]=${PRICING_FIELD}
    done
}

Which I call like this:

declare -A PRIVATE_FIELDS
update_array "myFile.json" "$(declare -p PRIVATE_FIELDS)"

However it doesn't work, the map remains empty.

echo ${PRIVATE_FIELDS["someKey"]}
>>> (empty)

I have tried literally each solution proposed in this answer but none of them worked. What am I doing wrong?

Bash version: 4.2.46(2)-release


Additional note, the Json file looks like this (apparently the calls to jq may be reduced):

{
    "name": "Something",
    "fields": [
        {
            "label": "key1",
            "value": "value1",
            "other": "other1"
        },
        {
            "label": "key2",
            "value": "value2",
            "other": "other2"
        }
    ]
}

Solution

  • When you use declare in a function, you're actually making the variable local. See help declare at a bash prompt.

    Use a nameref (requires bash version 4.3+):

    function update_array
    {
        local FILE_NAME=$1
        local -n MAP=$2     # MAP is now a _reference_ to the caller's variable
        # the rest stays the same
        for PRICING_FIELD in $(jq -c -r '.fields[]' "${FILE_NAME}")
        do
            FIELD_KEY=$(jq -r '.label' <<< "${PRICING_FIELD}")
            MAP["${FIELD_KEY}"]=${PRICING_FIELD}
        done
    }
    

    then you simply pass the array name

    declare -A PRIVATE_FIELDS
    update_array "myFile.json" PRIVATE_FIELDS
    
    declare -p PRIVATE_FIELDS
    

    To more efficiently iterate over the JSON file:

    $ jq -c -r '.fields[] | "\(.label)\t\(.)"' file.json
    key1    {"label":"key1","value":"value1","other":"other1"}
    key2    {"label":"key2","value":"value2","other":"other2"}
    

    That's assuming the labels don't contain any tab characters.


    Using that, plus your older bash version, you can do this

    Assuming that the result arrays will be in the global scope

    update_array() {
        local filename=$1 varname=$2
        local -A map
        while IFS=$'\t' read -r label json; do
            map[$label]=$json
        done < <(
            jq -c -r '.fields[] | "\(.label)\t\(.)"' "$filename"
        )
        eval declare -gA "$varname=$(declare -p map | cut -d= -f2-)"
    }
    

    You'd call it like

    $ echo $BASH_VERSION
    4.2.45(1)-release
    
    $ update_array tmp/file.json myArray
    
    $ declare -p myArray
    declare -A myArray='([key2]="{\"label\":\"key2\",\"value\":\"value2\",\"other\":\"other2\"}" [key1]="{\"label\":\"key1\",\"value\":\"value1\",\"other\":\"other1\"}" )'
    
    $ for label in "${!myArray[@]}"; do
    >     printf '"%s" => >>%s<<\n' "$label" "${myArray[$label]}"
    > done
    "key2" => >>{"label":"key2","value":"value2","other":"other2"}<<
    "key1" => >>{"label":"key1","value":"value1","other":"other1"}<<