63

How do you pass an associative array as an argument to a function? Is this possible in Bash?

The code below is not working as expected:

function iterateArray
{
    local ADATA="${@}"            # associative array

for key in "${!ADATA[@]}"
do
    echo "key - ${key}"
    echo "value: ${ADATA[$key]}"

done

}

Passing associative arrays to a function like normal arrays does not work:

iterateArray "$A_DATA"

or

iterateArray "$A_DATA[@]"
lecodesportif
  • 10,737
  • 9
  • 38
  • 58
niksfirefly
  • 749
  • 1
  • 5
  • 4
  • See here as a start (I'm not honestly sure if it matters that it's an associate array - it may make a big difference or none at all) http://stackoverflow.com/questions/1063347/passing-arrays-as-parameters-in-bash – Telemachus Nov 01 '10 at 13:30
  • 2
    @Telemachus: Those techniques won't work since the array elements are being passed without their indices. – Dennis Williamson Nov 01 '10 at 15:11
  • @Dennis So that means that it *does* make a big difference that it's an associate array, yes? At least, I think that's what your comment tells me. – Telemachus Nov 02 '10 at 12:15
  • @Telemachus: Yes, it does make a big difference since associative arrays are completely dependent on their indices. Using the techniques shown at the linked question discards the index which is OK on a contiguous, numerically-indexed array, but might would also fail on a sparse, numerically-indexed array if the indices are important (the array gets re-indexed contiguously in the receiving function). – Dennis Williamson Nov 02 '10 at 14:02
  • The answers below don't answer the question: *How to pass an associative array as argument to function?* – lecodesportif Sep 01 '11 at 15:53
  • Here is the corresponding question for regular bash "indexed" arrays: [Passing arrays as parameters in bash](https://stackoverflow.com/q/1063347/4561887) – Gabriel Staples Feb 10 '22 at 04:43

10 Answers10

61

If you're using Bash 4.3 or newer, the cleanest way is to pass the associative array by name reference and then access it inside your function using a name reference with local -n. For example:

function foo {
    local -n data_ref=$1
    echo ${data_ref[a]} ${data_ref[b]}
}

declare -A data
data[a]="Fred Flintstone"
data[b]="Barney Rubble"
foo data

You don't have to use the _ref suffix; that's just what I picked here. You can call the reference anything you want so long as it's different from the original variable name (otherwise youll get a "circular name reference" error).

juanmirocks
  • 4,786
  • 5
  • 46
  • 46
Todd Lehman
  • 2,880
  • 1
  • 26
  • 32
  • 2
    Thank you so much. This has to be the most simple way of dealing with AAs. You have saved me a lot of angst. – shahensha Jun 12 '20 at 03:27
  • 4
    How can I pass an associative array to another script? – shahensha Jun 30 '20 at 22:21
  • 1
    I'd use `declare -n` instead of `local -n`. – Artfaith Jun 26 '21 at 08:30
  • @F8ER — Interesting. Just looked that up; didn't realize that `declare` automatically creates a local variable when it's used in a function. Thanks! But what wasn't clear on the reference I found is how it's better than `local`. Is there a semantic difference or is it only a stylistic difference? – Todd Lehman Jun 26 '21 at 19:28
  • 1
    IMHO Semantic vs stylistic, I'd say, depends on the project. Personally, I wouldn't use both "keywords" at the same time (especially in small code snippets), but only one and while `local`'s functionality is limited, `declare` provides more features (it's newer). For example, in this example, using less definitions (language words) might highlight the issue better, but it's MHO. Related: https://stackoverflow.com/a/56628154/5113030 (> They exists because of the history...). For some reason I don't use `local`, knowing it may confuse another developer when they notice both. – Artfaith Jun 27 '21 at 13:12
  • 2
    Now that's interesting! I never tried it because the reference manual seemed to state the opposite: "The nameref attribute cannot be applied to array variables." in https://www.gnu.org/software/bash/manual/bash.html But as Galileo said ... "And yet it ... works with arrays!" – Colas Nahaboo Dec 15 '21 at 06:27
  • 1
    @ColasNahaboo, I saw that too! The **manual says this should NOT work on arrays!** So here is a follow-up question I just asked: [Why do the `man bash` pages state the `declare` and `local` `-n` attribute "cannot be applied to array variables", and yet it can?](https://stackoverflow.com/q/71059826/4561887) – Gabriel Staples Feb 10 '22 at 04:32
51

I had exactly the same problem last week and thought about it for quite a while.

It seems, that associative arrays can't be serialized or copied. There's a good Bash FAQ entry to associative arrays which explains them in detail. The last section gave me the following idea which works for me:

function print_array {
    # eval string into a new associative array
    eval "declare -A func_assoc_array="${1#*=}
    # proof that array was successfully created
    declare -p func_assoc_array
}

# declare an associative array
declare -A assoc_array=(["key1"]="value1" ["key2"]="value2")
# show associative array definition
declare -p assoc_array

# pass associative array in string form to function
print_array "$(declare -p assoc_array)" 
4LegsDrivenCat
  • 1,247
  • 1
  • 15
  • 24
Florian Feldhaus
  • 5,567
  • 2
  • 38
  • 46
  • +1 for the only solution which also works with locals (unlike pass by name). I think Bash 5 should improve on parameter passing constructs... – Werner Lehmann May 12 '12 at 20:23
  • 1
    Caution: newlines in mapped values are replaced with a space inside the function. – Werner Lehmann May 12 '12 at 22:47
  • 2
    Extending the double quotes around the `${1#*=}` fixes the whitespace issues. That said this isn't at all safe for arbitrary input. It *needs* to come from `declare -p` or it allows for arbitrary code execution. The pass-by-name version is safer. – Etan Reisner Oct 29 '15 at 01:45
  • What sort of evil hack is `${1#*=}`? Surely isn't regular Bash parameter expansion! – Noldorin Nov 25 '15 at 23:20
  • 1
    I do not understand why `${1#*=}` shouldn't be regular Bash parameter expansion. It's regular substring removal where the parameter is `$1` and the pattern is `*=`. – Florian Feldhaus Nov 26 '15 at 07:21
  • 6
    I couldn't get this to work and apparently since **Bash 4.3** there is `declare -n`. See this answer in another thread: http://stackoverflow.com/a/30894167/4162356 . – James Brown Jan 22 '16 at 12:31
  • @JamesBrown approach should be used for Bash >= 4.3 use `help declare` for more details. You might want to read `help local` as well. – theosp May 30 '16 at 04:22
  • 1
    is the eval necessary? Couldn't you just do `declare -A local func_assoc_array=${1#*=}` – solstice333 Mar 19 '18 at 06:11
  • @solstice333 I tried your suggestion and it seemed to work. I'm not sure if there are any sideeffects of using it, but a solution without using `eval`is better for sure. – Florian Feldhaus Mar 25 '18 at 10:28
  • @solstice333 without the eval, there is no declaration. admin@admin:~$ `function test () { declare -A ports=${1#*=}; echo -e "\nports = ${ports[@]}\n"; }` admin@admin:~$ `test "$(declare -p testPorts)"` ports = '([extDb]="8900" [pmaDisplay]="9000" [intDb]="3306" )' page22 ${var#pattern} "Use value of var after removing text matching pattern from the left. Remove the shortest matching piece." page 73 "eval forces variable expansion to happen first and then runs the resulting command..." https://inspirit.net.in/books/linux/Robbins%20-%20Bash%20Pocket%20Reference%20-%202010.pdf – Benjamin West Jul 02 '18 at 23:41
  • 2
    I'd not use eval, ever. – Artfaith Jun 26 '21 at 08:31
  • 1
    Use of eval. Could be bad, given the right scenario. – Anthony Rutledge Aug 12 '21 at 06:38
  • `It seems, that associative arrays can't be serialized or copied.` This isn't correct. You _can_ serialize them and pass them manually to bash functions, kind of like passing arrays in C, where you need to specify the length of the array too. [Here, I demonstrate manually passing associative arrays](https://stackoverflow.com/a/71060913/4561887) _by value_ in bash by passing the length, all keys, and all values. I also show passing associative arrays _by reference_. – Gabriel Staples Apr 03 '22 at 22:19
16

Based on Florian Feldhaus's solution:

# Bash 4+ only
function printAssocArray # ( assocArrayName ) 
{
    var=$(declare -p "$1")
    eval "declare -A _arr="${var#*=}
    for k in "${!_arr[@]}"; do
        echo "$k: ${_arr[$k]}"
    done

}

declare -A conf
conf[pou]=789
conf[mail]="ab\npo"
conf[doo]=456

printAssocArray "conf" 

The output will be:

doo: 456
pou: 789
mail: ab\npo
ling
  • 9,545
  • 4
  • 52
  • 49
9

Update, to fully answer the question, here is an small section from my library:

Iterating an associative array by reference

shopt -s expand_aliases
alias array.getbyref='e="$( declare -p ${1} )"; eval "declare -A E=${e#*=}"'
alias array.foreach='array.keys ${1}; for key in "${KEYS[@]}"'

function array.print {
    array.getbyref
    array.foreach
    do
        echo "$key: ${E[$key]}"
    done
}

function array.keys {
    array.getbyref
    KEYS=(${!E[@]})
}   

# Example usage:
declare -A A=([one]=1 [two]=2 [three]=3)
array.print A

This we a devlopment of my earlier work, which I will leave below.

@ffeldhaus - nice response, I took it and ran with it:

t() 
{
    e="$( declare -p $1 )"
    eval "declare -A E=${e#*=}"
    declare -p E
}

declare -A A='([a]="1" [b]="2" [c]="3" )'
echo -n original declaration:; declare -p A
echo -n running function tst: 
t A

# Output:
# original declaration:declare -A A='([a]="1" [b]="2" [c]="3" )'
# running function tst:declare -A E='([a]="1" [b]="2" [c]="3" )'
Orwellophile
  • 13,235
  • 3
  • 69
  • 45
4

You can only pass associative arrays by name.

It's better (more efficient) to pass regular arrays by name also.

aks
  • 2,328
  • 1
  • 16
  • 15
  • 3
    You would do something like `eval echo "\${$1[$key]}"` in the function, and pass in the name of the variable, without the `$`. – tripleee Sep 06 '11 at 10:18
3

Two ways to pass associative arrays as parameters in bash

For regular bash indexed arrays, see my two other answers here (by reference) and here (by value). For printing arrays by value or reference, see my answer here.

1. Manual passing (via serialization/deserialization) of the associative array

Here's another way: you can manually serialize the associative array as you pass it to a function, then deserialize it back into a new associative array inside the function:

Here's a full, runnable example from my eRCaGuy_hello_world repo:

array_pass_as_bash_parameter_2_associative.sh:

# Print an associative array using manual serialization/deserialization
# Usage:
#       # General form:
#       print_associative_array array_length array_keys array_values
#
#       # Example:
#       #                          length        indices (keys)    values
#       print_associative_array "${#array1[@]}" "${!array1[@]}" "${array1[@]}"
print_associative_array() {
    i=1

    # read 1st argument, the array length
    array_len="${@:$i:1}"
    ((i++))

    # read all key:value pairs into a new associative array
    declare -A array
    for (( i_key="$i"; i_key<$(($i + "$array_len")); i_key++ )); do
        i_value=$(($i_key + $array_len))
        key="${@:$i_key:1}"
        value="${@:$i_value:1}"
        array["$key"]="$value"
    done

    # print the array by iterating through all of the keys now
    for key in "${!array[@]}"; do
        value="${array["$key"]}"
        echo "  $key: $value"
    done
}

# Let's create and load up an associative array and print it
declare -A array1
array1["a"]="cat"
array1["b"]="dog"
array1["c"]="mouse"

#                         length         indices (keys)    values
print_associative_array "${#array1[@]}" "${!array1[@]}" "${array1[@]}"

Sample output:

  a: cat
  b: dog
  c: mouse

Explanation:

For a given function named print_associative_array, here is the general form:

# general form
print_associative_array array_length array_keys array_values

For an array named array1, here is how to obtain the array length, indices (keys), and values:

  1. array length: "${#array1[@]}"
  2. all of the array indices (keys in this case, since it's an associative array): "${!array1[@]}"
  3. all of the array values: "${array1[@]}"

So, an example call to print_associative_array would look like this:

# example call
#                         length         indices (keys)    values
print_associative_array "${#array1[@]}" "${!array1[@]}" "${array1[@]}"

Putting the length of the array first is essential, as it allows us to parse the incoming serialized array as it arrives into the print_associative_array function inside the magic @ array of all incoming arguments.

To parse the @ array, we'll use array slicing, which is described as follows (this snippet is copy-pasted from my answer here):

# array slicing basic format 1: grab a certain length starting at a certain
# index
echo "${@:2:5}"
#         │ │
#         │ └────> slice length
#         └──────> slice starting index (zero-based)

2. [Better technique than above!] Pass the array by reference

...as @Todd Lehman explains in his answer here

# Print an associative array by passing the array by reference
# Usage:
#       # General form:
#       print_associative_array2 array
#       # Example
#       print_associative_array2 array1
print_associative_array2() {
    # declare a local **reference variable** (hence `-n`) named `array_reference`
    # which is a reference to the value stored in the first parameter
    # passed in
    local -n array_reference="$1"

    # print the array by iterating through all of the keys now
    for key in "${!array_reference[@]}"; do
        value="${array_reference["$key"]}"
        echo "  $key: $value"
    done
}

echo 'print_associative_array2 array1'
print_associative_array2 array1
echo ""
echo "OR (same thing--quotes don't matter in this case):"
echo 'print_associative_array2 "array1"'
print_associative_array2 "array1"

Sample output:

print_associative_array2 array1
  a: cat
  b: dog
  c: mouse

OR (same thing--quotes don't matter in this case):
print_associative_array2 "array1"
  a: cat
  b: dog
  c: mouse

See also:

  1. [my answer] a more-extensive demo of me serializing/deserializing a regular "indexed" bash array in order to pass one or more of them as parameters to a function: Passing arrays as parameters in bash
  2. [my answer] a demo of me passing a regular "indexed" bash array by reference: Passing arrays as parameters in bash
  3. [my answer] array slicing: Unix & Linux: Bash: slice of positional parameters
  4. [my question] Why do the man bash pages state the declare and local -n attribute "cannot be applied to array variables", and yet it can?
  5. [my answer] How to manually pass an associative array to and from a subprocess in Bash
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
2

Here is a solution I came up with today using eval echo ... to do the indirection:

print_assoc_array() {
    local arr_keys="\${!$1[@]}" # \$ means we only substitute the $1
    local arr_val="\${$1[\"\$k\"]}"
    for k in $(eval echo $arr_keys); do #use eval echo to do the next substitution
        printf "%s: %s\n" "$k" "$(eval echo $arr_val)"
    done
}

declare -A my_arr
my_arr[abc]="123"
my_arr[def]="456"
print_assoc_array my_arr

Outputs on bash 4.3:

def: 456
abc: 123
Samuel Powell
  • 1,116
  • 1
  • 10
  • 8
2

yo:

 #!/bin/bash
   declare -A dict

   dict=(
    [ke]="va"
    [ys]="lu"
    [ye]="es" 
   )

   fun() {
     for i in $@; do
       echo $i
     done
    }

   fun ${dict[@]} # || ${dict[key]} || ${!dict[@] || ${dict[$1]} 

eZ

Nickotine
  • 167
  • 6
  • You get my vote! This is the simplest, most straightforward answer that actually answers the question - and works. Perhaps some of the heavyweights will take a look at this answer and comment on any potential security risks, expansions, etc. Personally I don't see any, but then I'm not a heavyweight. @Nickotine should add some explanation of the extra parameters that are commented out on the last line. – Prisoner 13 Sep 27 '17 at 23:27
  • There is one issue I just noticed... my array contains 6 fields per line (key, dbhost, dbuser, dbpasswd, dbname, "String of several words" and the first field is the (string index) key. The above loop processes each field, rather than each line. Any clever ways to have it process each line? I find that I have to rebuild the array by walking through the loop. Is that expected? I am in fact having trouble rebuilding it, and adding the 6th field multi-word string. It overwrites the original 5 field line when try to add the 6th field later. – Prisoner 13 Sep 28 '17 at 22:21
  • 1
    @Prisoner13, sorry I forgot about this if you've got 6 fields seperated by a space and quoted then just add this at the top and you'll get each line `IFS=$'\n'` – Nickotine Jan 12 '18 at 23:10
  • 3
    It only prints the values. – Kevin Whitefoot Jul 08 '20 at 13:06
  • @KevinWhitefoot Hmm, it’s supposed to print do you see the echo? This is equivalent, all the items in the array get printed 1 by 1, enlighten me if you can please? – Nickotine Jul 09 '20 at 21:57
  • @Nickotine. There doesn't seem to b a way of formatting code properly in comments but here goes ` kwhit@PonderStibbons ~ $ cat z #!/bin/bash declare -A dict dict=( [ke]="va" [ys]="lu" [ye]="es" ) fun() { for i in $@; do echo $i done } fun ${dict[@]} # || ${dict[key]} || ${!dict[@] || ${dict[$1]} kwhit@PonderStibbons ~ $ ./z lu va es kwhit@PonderStibbons ~ $ ` – Kevin Whitefoot Jul 10 '20 at 11:52
  • 1
    Boo, Python nomenclature. ;-) – Anthony Rutledge Aug 12 '21 at 05:41
  • @kevinwhitefoot… do you see the `#`? It’s a comment, all the options after the hash sign separated by `||` mean or this option etc… try `fun ${!dict[@]}` – Nickotine Aug 12 '21 at 14:34
1

Excellent. The simple solution described by @Todd Lehman, solved my associative array passing problem. I had to pass 3 parameters, an integer, an associative array and an indexed array to a function.

A while ago I'd read that since arrays in bash are not first-class entities, arrays could not be passed as arguments to functions. Clearly that's not the whole truth after all. I've just implemented a solution where a function handles those parameters, something like this...

function serve_quiz_question() {
    local index="$1"; shift
    local -n answers_ref=$1; shift
    local questions=( "$@" )

    current_question="${questions[$index]}"
    echo "current_question: $current_question"
    #...
    current_answer="${answers_ref[$current_question]}"
    echo "current_answer: $current_answer"
}

declare -A answers 
answers[braveheart]="scotland"
answers[mr robot]="new york"
answers[tron]="vancouver"
answers[devs]="california"

# integers would actually be assigned to index \
# by iterating over a random sequence, not shown here.
index=2
declare -a questions=( "braveheart" "devs" "mr robot" "tron"  )

serve_quiz_question "$index" answers "${questions[@]}"  

As the local variables get assigned, I had to shift the positional parameters away, in order to end with the ( "$@" ) assigning what's left to the indexed questions array.

The indexed array is needed so that we can reliably iterate over all the questions, either in random or ordered sequence. Associative arrays are not ordered data structures, so are not meant for any sort of predictable iteration.

Output:

current_question: mr robot
current_answer: new york
adebayo10k
  • 81
  • 5
-1

From the best Bash guide ever:

declare -A fullNames
fullNames=( ["lhunath"]="Maarten Billemont" ["greycat"]="Greg Wooledge" )
for user in "${!fullNames[@]}"
do
    echo "User: $user, full name: ${fullNames[$user]}."
done

I think the issue in your case is that $@ is not an associative array: "@: Expands to all the words of all the positional parameters. If double quoted, it expands to a list of all the positional parameters as individual words."

l0b0
  • 55,365
  • 30
  • 138
  • 223