Search code examples
bashevalassociative-array

Trying to assign a value to associative array within `eval` does not work


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'")

Solution

  • 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