Associative arrays in bash

If you do a google search for “associative arrays in bash”, you get a lot of hits that tell you that this is a feature not found in bash (though later versions may include this). Now, I find associative arrays (or hash tables if you like) a very nice feature to have available. And it is not that hard to emulate it in bash. One can use two arrays, one for the keys, and the other for the data elements, and a common counter for the indexes. A more ugly, but faster design could be direct string to variable name translation, with eval doing the dirty work.

Note that this kind of translation may be called seriously bad design, and in an interactive script, it may lead to serious security flaws. But if you know what the strings will look like, this short hand may do the work for you.

Consider the following code snippet:


#!/bin/bash
#
# Associative arrays in bash, take 1
# Using simple translation with eval

# Some test values with doublettes
values="a a a a b b c d";

# The incrementer for the keys to the array
key=-1 # Start with off-by-one, as incrementing is the first step in the loop

for val in $values; do

    # Need to check for uninitialized value, to get rid of doublettes
    # (and yes, here test(1) needs the extra character in the test)
    if eval [ 'x$'$val = "x" ]; then
        ((key++))
    fi

    # Now, fill the hash with values
    eval $val=$key;
    case $val in
        "a")
            arr[$key]="alice";
            ;;
        "b")
            arr[$key]="bob";
            ;;
        "c")
            arr[$key]="charlie";
            ;;
        "d")
            arr[$key]="diana";
            ;;
    esac
done

# Look up the value for a single key 'a'
key="a"
echo -n "Data entry for key $key is: "
eval key="$"$key
echo ${arr[$key]};

# Print the contents of the array
echo ""; echo "Whole array:"
for (( key=0; key < ${#arr[*]}; key++ ))
do
  echo ${arr[$key]}
done

## EOF


For more serious scripts, consider as mentioned, putting the keys in its own array, and search it while looking up values. This would take more time, though.

#!/bin/bash
#
# Associative arrays in bash, take 2
# Using two arrays

# Some test values with doublettes
values="a a a a b b c d";

# Search for existing keys
function getkey {
key=$1
for (( i=0; i < ${#keys[*]}; i++ )) {
if [ "${keys[$i]}" = "$key" ]; then
return $i
fi
}
# Not found, create a new key
keys[$i]=$key
return $i
}

# Now, fill the hash with values
for val in $values; do

# First, get a new key
getkey $val; key=$?

case $val in
"a")
arr[$key]="alice";
;;
"b")
arr[$key]="bob";
;;
"c")
arr[$key]="charlie";
;;
"d")
arr[$key]="diana";
;;
esac
done

# Look up the value for a single key 'd'
echo -n "Data entry for key 'd' is "
getkey 'd'; key=$?
echo ${arr[$key]};

# Print the complete contents of the hash
echo ""; echo "Whole hash:"
for (( i=0; i < ${#arr[*]}; i++ ))
do
echo -n "%arr{"
echo -n ${keys[$i]}
echo -n "}="
echo ${arr[$i]}
done

# EOF

12 Responses to “Associative arrays in bash”

  1. Dummy00001 says:

    I used on several occasions another hack. Or rather feature. My enlightenment was that bash (or sh) already has associative array – it’s internal variables (as printed by “set”).

    To emulate hash, you need to come up with some prefix to differentiate them from other variables and to allow to have several hashes.

    To insert pair ($KEY, $VALUE) into “hash” AAA:
    $ declare AAA__”${KEY}”=”${VALUE}”

    Trick: without “declare”, bash will interpret whole expression as a command. “declare” tells it to look at it as an assignment.

    To find an element by $KEY:
    $ eval $(echo echo $(echo \$AAA__$KEY))

    Unfortunately making find operation better is impossible. At least I do not know any way to do in bash something what e.g. make allows: ${$A} – iow accessing variable by its name. Had that worked, then find could have being written as “echo ${AAA__${KEY}}” instead of “eval echo echo” cruft. (As it is now, “eval echo echo” will also eat some spaces, potentially modifying value.)

  2. How about this:

    $ hash_insert ()
    {
    local name=$1 key=$2 val=$3
    eval __hash_$name_$key=$val
    }

    $ hash_find ()
    {
    local name=$1 key=$2
    local var=__hash_$name_$key
    echo ${!var}
    }

    $ hash_insert myhash key value

    $ hash_find myhash key
    value

    What do you think?

  3. (not sure why it didn’t preserve indents)

    btw, get all the keys with something like:

    hash_keys ()
    {
    local name=$1
    compgen -A variable __hash_$name
    }

    I’ve seen other example on the net that just “emulate” associative arrays but still are not O(1) as a hash should be. This implementation should actually be since it uses existing bash internal hashing algorithm for looking up variable names (as original author notes). I don’t like the namespace pollution but it’s probably the best we can do. Also note, those obviously won’t deal with any special characters, but are just for demo purposes.

  4. Rick Richardson says:

    Typo’s in the above…

    hash_insert ()
    {
    local name=$1 key=$2 val=$3
    eval __hash_${name}_${key}=$val
    }
    hash_find ()
    {
    local name=$1 key=$2
    local var=__hash_${name}_${key}
    echo ${!var}
    }
    hash_keys ()
    {
    local name=$1
    compgen -A variable __hash_${name} | sed “s/__hash_${name}_//”
    }

  5. Vito Tafuni says:

    what about this???

    function idx { eval ‘case $1 in ‘${cases[*]}’ *) [ "$1" ] &’\””‘\”$1′\””) echo ‘${#cases[*]}’;;’\” ); echo ‘${#cases[*]}’;}; esac’; }

    idx all
    0
    idx all
    0
    idx jhon
    1
    idx all
    0

    as you can see the function return different values for different strings
    so associative array becomes quite simple like a simple array

    array[`idx name`]=value
    echo ${array[`idx name`]}
    value

  6. Chris says:

    Vito,

    The formatter munged up your code. Is there a way you could repaste it so I can give it a try?

  7. Mark says:

    -bash-3.00$ idx all
    -bash: command substitution: line 1: syntax error near unexpected token `*’
    -bash: command substitution: line 1: `case $1 in ‘${cases[*]}’ *) [ "$1" ] &’\”"‘\”$1′\”") echo ‘${#cases[*]}’;;’\” ); echo ‘${#cases[*]}’;}; esac’
    -bash-3.00$ array[`idx name`]=value
    -bash: command substitution: line 0: syntax error near unexpected token `*’
    -bash: command substitution: line 0: `case $1 in ‘${cases[*]}’ *) [ "$1" ] &’\”"‘\”$1′\”") echo ‘${#cases[*]}’;;’\” ); echo ‘${#cases[*]}’;}; esac’
    -bash-3.00$ echo ${array[`idx name`]}
    -bash: command substitution: line 0: syntax error near unexpected token `*’
    -bash: command substitution: line 0: `case $1 in ‘${cases[*]}’ *) [ "$1" ] &’\”"‘\”$1′\”") echo ‘${#cases[*]}’;;’\” ); echo ‘${#cases[*]}’;}; esac’
    value
    -bash-3.00$

  8. Vito Tafuni says:

    sorry for late answering but the editor changes altgr + single quote with ‘ so change them to have the working version!!

    however this is the last powerfull version of the index function i’ve created

    # index function for associative arrays
    function idx () { CASES=`ls -t /tmp/$$_$2_CASES.* 2>/dev/null | head -n1`; [ $(eval echo \${#$2[*]}) -eq 0 ] && CASES=`mktemp /tmp/$$_$2_CASES.XXXXXX`; eval ‘case $1 in ‘$(cat $CASES)’ *) [ "$1" ] & esac’; }

  9. Manolo says:

    To use the value for a key in the hash in a script, call this function with the variable to get the value in as third argument

    hash_get() {
    local name=$1 key=$2 v
    eval “v=__hash_${name}_${key}”
    eval “$3=\$$v”
    }

  10. lingtalfi says:

    @Scott Mcdermott
    Amazing!!!
    That allows us to pass associative arrays to functions.

  11. lingtalfi says:

    Based on Scott Mcdermott’s code,
    here is my variation, using two numerical arrays, one for the keys, and one for the values (so that we can get rid of the limitations on the key format).

    #!/bin/bash

    function assoc_create # ( arrayName )
    {
    if [ 1 -eq $# ]; then
    eval $1=\(\)
    eval ${1}_keys=\(\)
    else
    echo “assoc_create: invalid number of arguments (Usage: assoc_create )” >&2
    exit 1
    fi
    }

    function assoc_add # ( arrayName, key, value )
    {
    if [ 3 -eq $# ]; then
    local arrKeys=”${1}_keys”
    eval $arrKeys+=\(\”"$2″\”\)

    local arr=”${1}”
    eval $arr+=\(\”"$3″\”\)
    else
    echo “assoc_add: invalid number of arguments (Usage: assoc_add )” >&2
    exit 1
    fi
    }

    # if no default is provided, this function exit in case of failure
    function assoc_get # ( arrayName, key, ?default)
    {

    if [ 2 -eq $# -o 3 -eq $# ]; then

    eval arrKeys=\${“${1}_keys”[@]}
    eval arr=\(\${“${1}”[@]}\)
    local i=0
    local found=0
    for key in ${arrKeys[@]}
    do
    if [ "$key" = "$2" ]; then
    found=1
    break
    else
    (( i++ ))
    fi
    done

    if [ 1 -eq $found ]; then
    echo ${arr[$i]}
    elif [ 3 -eq $# ]; then
    echo “$3″
    else
    echo “assoc_get: key not found: $2″ >&2
    exit 1
    fi
    else
    echo “assoc_get: invalid number of arguments (Usage: assoc_get ?)” >&2
    exit 1
    fi
    }

    assoc_create pou
    assoc_add pou key1 value1
    assoc_add pou key2 value2
    assoc_add pou key3 value3

    value=$(assoc_get pou key2)
    echo “last: $?”
    echo “value=$value”

    echo ————————
    for i in ${pou_keys[@]}; do
    echo “key=$i”
    done

    for i in ${pou[@]}; do
    echo “value=$i”
    done

Leave a Reply