hollabaloo
hollabaloo

Reputation: 853

Create associative array in bash 3

After thoroughly searching for a way to create an associative array in bash, I found that declare -A array will do the trick. But the problem is, it is only for bash version 4 and the bash version the server has in our system is 3.2.16.

How can I achieve some sort of associative array-like hack in bash 3? The values will be passed to a script like

ARG=array[key];

./script.sh ${ARG}

EDIT: I know that I can do this in awk, or other tools but strict bash is needed for the scenario I am trying to solve.

Upvotes: 42

Views: 43230

Answers (7)

alfredocambera
alfredocambera

Reputation: 3410

Here is my take:

#!/usr/bin/env bash

hash_to_value() {
    case $1 in
        'dev')     echo "dev-hash";;
        'stage')   echo "stage-hash";;
        'sandbox') echo "sandbox-hash";;
        'prod')    echo "prod-hash";;
    esac
}

value=$(hash_to_value "dev")

echo "${value}"

The output of the execution:

$ /tmp/hash.sh
dev-hash

I know, using a case is ugly. But, I found it easy to understand and implement.

Upvotes: 0

Zac
Zac

Reputation: 4695

Ended up creating two arrays:

HOST_LIST=(host1 host2)
URL_LIST=(url1 url2)

then using the index:

for i in ${!HOST_LIST[@]}; do
    echo ${HOST_LIST[$i]} mapped to ${URL_LIST[$i]}
done

More useful an extraction function:

function GetUrl()
{
    local _HOSTNAME=$1
    local retVal=$2
    for i in ${!HOST_LIST[@]}; do
        if [ "${HOST_LIST[$i]}" = "$_HOSTNAME" ]; then
            eval $retVal="'${URL_LIST[$i]}'"
            return 0
        fi
    done
    return 1
}

Called as:

GetUrl host2 resultUrl
echo $resultUrl

Upvotes: 0

Janos Barbero
Janos Barbero

Reputation: 31

This turns out to be ridiculously easy. I had to convert a bash 4 script that used a bunch of associative arrays to bash 3. These two helper functions did it all:

array_exp() {
    exp=${@//[/__}
    eval "${exp//]}"
}

array_clear() {
    unset $(array_exp "echo \${!$1__*}")
}

I'm flabbergasted that this actually works, but that's the beauty of bash. E.g.

((all[ping_lo] += counts[ping_lo]))

becomes

array_exp '((all[ping_lo] += counts[ping_lo]))'

Or this print statement:

printf "%3d" ${counts[ping_lo]} >> $return

becomes

array_exp 'printf "%3d" ${counts[ping_lo]}' >> $return

The only syntax that changes is clearing. This:

counts=()

becomes

array_clear counts

and you're set. You could easily tell array_exp to recognize expressions like "=()" and handle them by rewriting them as array_clear expressions, but I prefer the simplicity of the above two functions.

Upvotes: 2

Bash 3 has no associative arrays, so you're going to have to use some other language feature(s) for your purpose. Note that even under bash 4, the code you wrote doesn't do what you claim it does: ./script.sh ${ARG} does not pass the associative array to the child script, because ${ARG} expands to nothing when ARG is an associative array. You cannot pass an associative array to a child process, you need to encode it anyway.

You need to define some argument passing protocol between the parent script and the child script. A common one is to pass arguments in the form key=value. This assumes that the character = does not appear in keys.

You also need to figure out how to represent the associative array in the parent script and in the child script. They need not use the same representation.

A common method to represent an associative array is to use separate variables for each element, with a common naming prefix. This requires that the key name only consists of ASCII letters (of either case), digits and underscores. For example, instead of ${myarray[key]}, write ${myarray__key}. If the key is determined at run time, you need a round of expansion first: instead of ${myarray[$key]}, write

n=myarray__${key}; echo ${!n}

For an assignment, use printf -v. Note the %s format to printf to use the specified value. Do not write printf -v "myarray__${key}" %s "$value" since that would treat $value as a format and perform printf % expansion on it.

printf -v "myarray__${key}" %s "$value"

If you need to pass an associative array represented like this to a child process with the key=value argument representation, you can use ${!myarray__*} to enumerate over all the variables whose name begins with myarray__.

args=()
for k in ${!myarray__*}; do
  n=$k
  args+=("$k=${!n}")
done

In the child process, to convert arguments of the form key=value to separate variables with a prefix:

for x; do
  if [[ $x != *=* ]]; then echo 1>&2 "KEY=VALUE expected, but got $x"; exit 120; fi
  printf -v "myarray__${x%%=*}" %s "${x#*=}"
done

By the way, are you sure that this is what you need? Instead of calling a bash script from another bash script, you might want to run the child script in a subshell instead. That way it would inherit from all the variables of the parent.

Upvotes: 38

Lloeki
Lloeki

Reputation: 6713

If you don't want to handle a lot of variables, or keys are simply invalid variable identifiers, and your array is guaranteed to have less than 256 items, you can abuse function return values. This solution does not require any subshell as the value is readily available as a variable, nor any iteration so that performance screams. Also it's very readable, almost like the Bash 4 version.

Here's the most basic version:

hash_index() {
    case $1 in
        'foo') return 0;;
        'bar') return 1;;
        'baz') return 2;;
    esac
}

hash_vals=("foo_val"
           "bar_val"
           "baz_val");

hash_index "foo"
echo ${hash_vals[$?]}

More details and variants in this answer

Upvotes: 7

Bubnoff
Bubnoff

Reputation: 4097

Here is another post/explanation on associative arrays in bash 3 and older using parameter expansion:
https://stackoverflow.com/a/4444841

Gilles' method has a nice if statement to catch delimiter issues, sanitize oddball input ...etc. Use that.

If you are somewhat familiar with parameter expansion:
http://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html

To use in your scenario [ as stated: sending to script ]: Script 1: sending_array.sh

# A pretend Python dictionary with bash 3 
ARRAY=( "cow:moo"
        "dinosaur:roar"
        "bird:chirp"
        "bash:rock" )

bash ./receive_arr.sh "${ARRAY[@]}"

Script 2: receive_arr.sh

argAry1=("$@")

function process_arr () {
    declare -a hash=("${!1}")
    for animal in "${hash[@]}"; do
        echo "Key: ${animal%%:*}"
        echo "Value: ${animal#*:}"
    done
}

process_arr argAry1[@]

exit 0

Method 2, sourcing the second script: Script 1: sending_array.sh

source ./receive_arr.sh
# A pretend Python dictionary with bash 3 
ARRAY=( "cow:moo"
        "dinosaur:roar"
        "bird:chirp"
        "bash:rock" )

process_arr ARRAY[@]

Script 2: receive_arr.sh

function process_arr () {
    declare -a hash=("${!1}")
    for animal in "${hash[@]}"; do
        echo "Key: ${animal%%:*}"
        echo "Value: ${animal#*:}"
    done
}

References:
Passing arrays as parameters in bash

Upvotes: 12

Aaron Digulla
Aaron Digulla

Reputation: 328594

You can write the key-value pairs to a file and then grep by key. If you use a pattern like

key=value

then you can egrep for ^key= which makes this pretty safe.

To "overwrite" a value, just append the new value at the end of the file and use tail -1 to get just the last result of egrep

Alternatively, you can put this information into a normal array using key=value as value for the array and then iterator over the array to find the value.

Upvotes: 2

Related Questions