Home > Software design >  Trying to assign a value to associative array within `eval` does not work
Trying to assign a value to associative array within `eval` does not work

Time:11-18

When trying to define helper functions to build up associative lists, I get an error, I cannot resolve myself (BASH 4.4):

/tmp/foo.sh: line 18: 'key': syntax error: operand expected (error token is "'key'")

For BASH 4.3 I got:

/tmp/foo.sh: line 18: key: unbound variable

Here is the test case:

#!/bin/bash
set -u

# add services list
add_list()
{
    local list="$1"

    eval "declare -a ${list}=(); declare -A ${list}_A=()"
}

# add services to list of services
add_service()
{
    local list="$1" def="$2"
    local s="${def%%:*}"

    eval "${list} =('$def'); ${list}_A['$s']='$def'"
}

add_list TEST
add_service TEST 'key:value'

The reason for the two array is that I want to access elements by key, and I want to preserve the original ordering (actually ${list} =('$s') would be sufficient for that).

Here is the output of bash -x:

> bash -x /tmp/foo.sh 
  set -u
  add_list TEST
  local list=TEST
  eval 'declare -a TEST=(); declare -A TEST_A=()'
   TEST=()
   declare -a TEST
   TEST_A=()
   declare -A TEST_A
  add_service TEST key:value
  local list=TEST def=key:value
  local s=key
  eval 'TEST =('\''key:value'\''); TEST_A['\''key'\'']='\''key:value'\'''
   TEST =('key:value')
   TEST_A['key']=key:value
/tmp/foo.sh: line 18: 'key': syntax error: operand expected (error token is "'key'")

CodePudding user response:

NOTES:

  • skipping discussion on why eval may not be the best approach
  • skipping discussion on an alternative approach that uses namerefs
  • will focus on how OP's current code is (not) creating the desired array and a quick fix

Arrays declared in functions remain locally scoped unless the array is also declared with the global flag; consider the following:

$ mytest() { typeset -a myarray; typeset -p myarray; echo "##### mytest(): exit"; }
                     ^^
$ unset myarray
$ mytest
declare -a myarray                           # array exists while inside the function
##### mytest(): exit

$ typeset -p myarray
-bash: typeset: myarray: not found           # array no longer exists once outside the function

Now add the -g flag:

$ mytest() { typeset -ag myarray; typeset -p myarray; echo "##### mytest(): exit"; }
                     ^^^
$ unset myarray
$ mytest
declare -a myarray                           # array exists while inside the function
##### mytest(): exit

$ typeset -p myarray
declare -a myarray                           # array still exists after leaving function

Adding the -g flag to both array declarations in OP's current function:

add_list()
{
    local list="$1"

    eval "declare -ag ${list}=(); declare -Ag ${list}_A=()"
    #             ^^^                     ^^^
}

NOTE: the add_service function definition can remain as is for now

Running OP's test:

$ unset TEST TEST_A
$ add_list TEST
$ add_service TEST 'key:value'
$ typeset -p TEST TEST_A
declare -a TEST=([0]="key:value")
declare -A TEST_A=([key]="key:value" )

As for why OP's current code generates an error ...

At the command prompt we'll emulate the add_service operation ...

$ unset TEST TEST_A                                       # just to make these variables are undefined before calling add_service ...

$ typeset -p TEST TEST_A                                  # verify variables are not set
-bash: typeset: TEST: not found
-bash: typeset: TEST_A: not found

$ TEST =('key:value')                                     # bash recognizes this as valid array syntax and will automagically create a normal (-a) array named TEST
$ typeset -p TEST
declare -a TEST=([0]="key:value")

$ TEST_A['key']='key:value'                               # bash recognizes this as the correct syntax for an integer-keyed array but has problems processing the string `key` as an integer so ...
-bash: 'key': syntax error: operand expected (error token is "'key'")

$ TEST_A[key]='key:value'                                 # again, looks like correct syntax but in this case no error ...
$ typeset -p TEST_A
declare -a TEST_A=([0]="key:value")
        ^^

        # in this case bash considers key as a variable (ie, bash treats it as $key)
        # but since $key is undefined it defaults to 0 and a normal array (-a) is
        # created with index 0

$ TEST_A[xxx]='keyX:valueX'
$ typeset -p TEST_A
declare -a TEST_A=([0]="keyX:valueX")                     # $xxx is undefined, treated as 0, and we end up overwriting previous 0-indexed entry in array

$ key=9
$ TEST_A[key]='key:value'
$ typeset -p TEST_A
declare -a TEST_A=([0]="keyX:valueX" [9]="key:value")     # $key is defined (9) so we get a new array entry with index=9

CodePudding user response:

Using declare in a function makes the variable local, so it's probably impossible to define a new associative array in the parent scope from within a function.

For a normal array it's possible, check just the result of this:

#!/bin/bash

make_it_an_array() {
    [[ $1 =~ ^[[:alpha:]_][[:alnum:]_]*$ ]] || return 1
    eval "$1=()"
}

fun1() {
    local arr # arr isn't an array, so what will happen?
    make_it_an_array arr
    printf '"%s" in %s: %s\n' "$1" "$FUNCNAME" "$(declare -p arr 2> /dev/null)"
}

#############################################################

fun1 arr
printf '"arr" in main: %s\n' "$(declare -p arr 2> /dev/null)"
"arr" in fun1: declare -a arr='()'
"arr" in main: 

And if you replace eval "$1=()" with declare -ga "$1=()" the result is:

"arr" in fun1: declare -a arr='()'
"arr" in main: declare -a arr='()'

which means that the local scope of the variable in the parent function is lost


Aside:

If $list only contains the name of a variable then you can replace:

eval "declare -a ${list}=(); declare -A ${list}_A=()"

With:

declare -a "$list=()"
declare -A "${list}_A=()"
  • Related