22

I need to loop over an associative array and drain the contents of it to a temp array (and perform some update to the value).

The leftover contents of the first array should then be discarded and i want to assign the temp array to the original array variable.

Sudo code:

declare -A MAINARRAY
declare -A TEMPARRAY
... populate ${MAINARRAY[...]} ...

while something; do     #Drain some values from MAINARRAY to TEMPARRAY
    ${TEMPARRAY["$name"]}=((${MAINARRAY["$name"]} + $somevalue))
done
... other manipulations to TEMPARRAY ...

unset MAINARRAY        #discard left over values that had no update
declare -A MAINARRAY
MAINARRAY=${TEMPARRAY[@]}  #assign updated TEMPARRAY back to MAINARRAY (ERROR HERE)
codeforester
  • 39,467
  • 16
  • 112
  • 140
David Parks
  • 30,789
  • 47
  • 185
  • 328
  • See here: http://stackoverflow.com/questions/3112687/how-to-iterate-over-assoziative-array-in-bash –  Jul 12 '11 at 13:28
  • Yeh, that's what I've got now, was thinking there should be some more efficient way to rename the variable. – David Parks Jul 12 '11 at 14:15
  • 1
    The solution by [FlorianFeldhaus] would work if just variable renaming was needed. Infact, it could be a single statement with a little help from sed like this: `eval $(declare -p old_name |sed 's/old_name=/new_name=/')` – Samveen Jun 06 '13 at 23:12

8 Answers8

32

Copying associative arrays is not directly possible in bash. The best solution probably is, as already been pointed out, to iterate through the array and copy it step by step.

There is another solution which I used to pass variables to functions. You could use the same technique for copying associative arrays:

# declare associative array
declare -A assoc_array=(["key1"]="value1" ["key2"]="value2")
# convert associative array to string
assoc_array_string=$(declare -p assoc_array)
# create new associative array from string
eval "declare -A new_assoc_array="${assoc_array_string#*=}
# show array definition
declare -p new_assoc_array
Community
  • 1
  • 1
Florian Feldhaus
  • 5,567
  • 2
  • 38
  • 46
  • The `eval` seems to be necessary. Why is that? – Michael Hoffman Oct 31 '12 at 22:36
  • 2
    The ``eval'' is necessary because it declares the new array. The problem is, that declare -A expect a statement in the assignement and not a string. Thus you need to create a string and evaluate it as a statement. – Florian Feldhaus Nov 01 '12 at 10:52
  • 1
    More important is that `declare -A` is absolutely necessary — without it (for example, inside a function working with arrays from parent environment rather than creating local variables) the whole trick does not work, as it will only create a single item with key `0` and value `(["key1"]="value1" ["key2"]="value2" )`, because escaping rules seem incompatible between `declare` and *assignment*. – Anton Samsonov Jun 07 '16 at 17:46
  • 1
    This should be selected as the answer. It works, and it's WAY faster that iterating. – Fmstrat May 16 '18 at 15:17
  • if you want to understand better the '#*=', it's well explained here : http://tldp.org/LDP/abs/html/string-manipulation.html ('Substring Removal' section) – Gremi64 Sep 12 '18 at 09:02
  • The `eval` works better with whitespace in the array values if you include the `assoc_array_string` in the double-quotes, i.e. `eval "declare -A new_assoc_array=${assoc_array_string#*=}"`. – wjandrea Nov 07 '18 at 20:28
  • eval is not strictly necessary: `declare -A "new_assoc_array=${assoc_array_string#*=}"` suffices. – glenn jackman Jun 17 '21 at 17:57
13

With associative arrays, I don't believe there's any other method than iterating

for key in "${!TEMPARRAY[@]}"  # make sure you include the quotes there
do
  MAINARRAY["$key"]="${TEMPARRAY["$key"]}"
  # or: MAINARRAY+=( ["$key"]="${TEMPARRAY["$key"]}" )
done
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • And how do you do that in a function? Where you only get $1 and $2 as variable names (because there's now way to pass the actual variable)? – cprn May 22 '17 at 12:39
8

This one-liner does an associative array copy: MAINARRAY=TEMPARRAY

eval $(typeset -A -p TEMPARRAY|sed 's/ TEMPARRAY=/ MAINARRAY=/')
tombrus
  • 369
  • 3
  • 6
7

Here is a small Copy-Function for bash-Variables of any kind
- normal scalar variables
- indexed arrays
- associative arrays

### Function vcp    -VariableCoPy-  
# $1 Name of existing Source-Variable  
# $2 Name for the Copy-Target  
vcp() {
    local var=$(declare -p $1)
    var=${var/declare /declare -g }
    eval "${var/$1=/$2=}"
}

Usage, Examples:

# declarations
var="  345  89  "
ind_array=(Betty "  345  89  ")
declare -A asso_array=([one]=Harry [two]=Betty [some_signs]=" +*.<\$~,'/ ")  

# produce the copy
vcp var varcopy
vcp ind_array ind_array_copied
vcp asso_array asso_array_2   

# now you can check the equality between original and copy with commands like
# declare -p <name>

The results

--3    1: "${asso_array[@]}"   
(5)       asso_array[one]:        |Harry|   
(11)      asso_array[some_signs]: | +*.<$~,'/ |   
(5)       asso_array[two]:        |Betty|   
--3    4: "${asso_array_2[@]}"   
(5)       asso_array_2[one]:        |Harry|   
(11)      asso_array_2[some_signs]: | +*.<$~,'/ |   
(5)       asso_array_2[two]:        |Betty|   
--2    7: "${ind_array[@]}"   
(5)       ind_array[0]:   |Betty|   
(11)      ind_array[1]:   |  345  89  |   
--2    9: "${ind_array_copied[@]}"   
(5)       ind_array_copied[0]:   |Betty|   
(11)      ind_array_copied[1]:   |  345  89  |   
(11)  11: "$var":   |  345  89  |  
(11)  12: "$varcopy":   |  345  89  |  
bashly
  • 79
  • 2
  • 2
  • wonderful! This vcp function works like a charm. However, would you mind annotating the 3 lines of code? I looked up bash declare -p and not sure what it is doing here in line 1. Then what does the slash in line 2 and line 3 do? Thx – HAltos Apr 25 '22 at 19:51
  • OK. Here is what I found out: The first line of vcp echo the complete line of the associate array $1 (like declare -A array1 ... ) The second line works just like sed, and var=${var/declare /declare -g } is equivalent to the following var=$(echo var | sed -e 's/declare /declare -g /' ) The third line similarly use var substitution like sed. Very cool!''' – HAltos Apr 25 '22 at 21:42
2

Following both the suggestions of glenn jackman and ffeldhaus, you can build a function which might become handy:

function cp_hash
{
    local original_hash_name="$1"
    local copy_hash_name="$2"

    local __copy__=$(declare -p $original_hash_name);
    eval declare -A __copy__="${__copy__:$(expr index "${__copy__}" =)}";

    for i in "${!__copy__[@]}"
    do
        eval ${copy_hash_name}[$i]=${__copy__[$i]}
    done
}


Usage:

declare -A copy_hash_name
cp_hash 'original_hash_name' 'copy_hash_name'


Example:

declare -A hash
hash[hello]=world
hash[ab]=cd

declare -A copy
cp_hash 'hash' 'copy'

for i in "${!copy[@]}"
do
    echo "key  : $i | value: ${copy[$i]}"
done


Will output

key  : ab | value: cd
key  : hello | value: world
Community
  • 1
  • 1
Luca Borrione
  • 16,324
  • 8
  • 52
  • 66
  • upvoted even though (`eval declare -A __copy__="${__copy__:$(expr index "${__copy__}" =)}";`) explodes for me - still a good starting point. – keen Aug 09 '16 at 22:05
  • If your array contains regular expression like 's/([a-zA-Z]*).*/\1/p', this will not work. – fab Apr 03 '18 at 19:20
2

How about this one (Doesn't create a real copy, just a link to source variable):

#!/bin/bash
declare -A my_array=(["key1"]="value1" ["key2"]="value2")
declare -n arr=my_array
arr['LOG_FILE']=/tmp/log.txt
echo ${arr['key1']}
echo ${arr['LOG_FILE']}

Will print:

value1
/tmp/log.txt
fab
  • 330
  • 3
  • 6
  • 1
    The nameref attribute cannot be applied to array variables. see https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-declare – Feng Sep 18 '18 at 00:43
  • You're right but this solution works in my bash version: 4.3.48(1) – fab Sep 18 '18 at 19:03
1

expanding on Luca Borrione's cp_hash - which didn't work for me, and I gave up trying to track down the eval expansion issue - I ran into differences before and after bash 4.2. after 4.2(something) this gets a lot easier... but that's not backwards compatible. See 1 and 2

so my variation tested on 4.1.2(1) and 4.3.46(1):

#!/bin/bash
## bash4 due to associative arrays!

    function cp_hash() {
        ## REQUIRES you to declare -A $2 in advance.
        local original_hash_name="$1"
        local copy_hash_name="$2"
        #
        # sadly we have no way to identify if you have already declared it, so bull ahead.
        #
        ## store the definition of the old array
        local __copy__=$(declare -p $original_hash_name)
        ## rename the array inside the definition
        __copy__=${__copy__/${original_hash_name}=/__copy__=}

        ## for bash 4.2 > we could end here.
        ## declare -A creates local scope variables by default, so add -g
        ## this DOES NOT work prior to 4.2, even w/o -g and w/ a declare outside.
        #    __copy__=${__copy__/${original_hash_name}=/${copy_hash_name}=}
        #    eval ${__copy__/-A/-g -A}

        ## for bash4 where we can't do -g, then:
        ## local associative array based on the definition we stored and modified
        eval ${__copy__}
        ## loop through the local copy, and store it in the declared-outside copy.
        for i in "${!__copy__[@]}"
        do
            eval ${copy_hash_name}[$i]=${__copy__[$i]}
        done
    }

    declare -A hash
    hash[hello]=world
    hash[ab]=cd

    #not required for 4.2+ if you use -g, neither helps nor hinders
    declare -A copy

    cp_hash 'hash' 'copy'

    echo hash: ${hash[@]}
    echo copy: ${copy[@]}

    echo "copy result loop"
    for i in "${!copy[@]}"
    do
        echo "key  : $i | value: ${copy[$i]}"
    done
Community
  • 1
  • 1
keen
  • 816
  • 10
  • 11
  • 1
    If your array contains regular expression like 's/\([a-zA-Z]*\).*/\1/p', this will not work. – fab Apr 03 '18 at 19:19
  • @fab do you have any suggested improvements that might help that case? – keen Apr 11 '18 at 20:38
  • 1
    In my case Florian Feldhaus solution works. You can use also n parameter for declare. declare -n params=HISTORY_PARAM_MAP – fab Apr 19 '18 at 14:48
-2
MAINARRAY=( "${TEMPARRAY[@]}" )
Adam Bryzak
  • 2,608
  • 2
  • 19
  • 7
  • 1
    That's what I thought, but I get an error on that line: "must use subscript when assigning associative array" – David Parks Jul 12 '11 at 06:27