4

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"
        }
    ]
}
Matteo NNZ
  • 11,930
  • 12
  • 52
  • 89
  • `$1` inside a function is the first argument passed to it, in your case `"$(declare -p PRIVATE_FIELDS)"` but your code seems to be referring to the first argument of the script. – LMC Jul 22 '21 at 16:50
  • @LMC sorry I pasted the wrong example, I've updated my question correctly. Thanks for spotting – Matteo NNZ Jul 22 '21 at 16:52
  • You could use PRIVATE_FIELDS inside the function, no need to create another var. – LMC Jul 22 '21 at 16:55
  • @LMC thanks, it worked!! If you write it as an answer I'll accept it – Matteo NNZ Jul 22 '21 at 16:59
  • @LMC sorry, wrote too fast. Indeed it works, but I need to explicitly write PRIVATE_FIELDS in the body of the function, which makes the refactoring useless because I will need to have one function per map I need to update. – Matteo NNZ Jul 22 '21 at 17:01
  • 1
    BTW, the `function` keyword is something bash supports for backwards compatibility with pre-POSIX ksh. Better to just use `update_array() {` with no `function` to write code that's going to be more compatible with other POSIX-family shells; see **both relevant entries** in https://wiki.bash-hackers.org/scripting/obsolete (the first one talks about `function update_array() {`, which isn't compatible with either ksh or POSIX; the second one talks about the `function update_array {` legacy-ksh form). – Charles Duffy Jul 22 '21 at 17:37
  • A nameref variable will help, and you can declare it in the function to eliminate a lot of that extra structure. c.f. https://stackoverflow.com/questions/68167187/how-to-use-a-bash-variable-reference-to-an-associative-array-in-a-bash-function/68168637#68168637 – Paul Hodges Jul 22 '21 at 17:41
  • 1
    @PaulHodges, the OP has a bash release too old to support namerefs (hence the answer by glenn not being accepted, and the question being updated to reflect the version they're running). – Charles Duffy Jul 22 '21 at 17:42
  • @CharlesDuffy thanks for the tip, I don't often use Bash so I miss a lot of good practices, it's always good to learn them! – Matteo NNZ Jul 22 '21 at 18:16

1 Answers1

5

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"}<<
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • Show the JSON data, we can reduce the number of times you need to call out to `jq` to once. – glenn jackman Jul 22 '21 at 17:02
  • 3
    Get out of the habit of using ALLCAPS variable names, leave those as reserved by the shell. One day you'll write `PATH=something` and then [wonder why](https://stackoverflow.com/q/27555060/7552) your [script is broken](https://stackoverflow.com/q/28310594/7552). – glenn jackman Jul 22 '21 at 17:03
  • Thanks for your answer! About the naming, thanks for the tip. I'm a very occasional bash scripter, I don't really know the good practices :) About the Json, I'll update my question to show it. Then I'll try your solution and let you know if it worked, thanks again! – Matteo NNZ Jul 22 '21 at 17:05
  • I see your bash version, I'll update my answer shortly – glenn jackman Jul 22 '21 at 17:10
  • Thanks a lot not only for the perfectly working solution but also for the nice tips, my script really looks much more beautiful now :) – Matteo NNZ Jul 22 '21 at 18:31