Как да комбинирам асоциативни масиви в bash?

Някой знае ли за елегантен начин за комбиниране на два асоциативни масива в bash точно както бихте направили с нормален масив? Ето за какво говоря:

В bash можете да комбинирате два нормални масива, както следва:

declare -ar array1=( 5 10 15 )
declare -ar array2=( 20 25 30 )
declare -ar array_both=( ${array1[@]} ${array2[@]} )

for item in ${array_both[@]}; do
    echo "Item: ${item}"
done

Искам да направя същото с два асоциативни масива, но следният код не работи:

declare -Ar array1=( [5]=true [10]=true [15]=true )
declare -Ar array2=( [20]=true [25]=true [30]=true )
declare -Ar array_both=( ${array1[@]} ${array2[@]} )

for key in ${!array_both[@]}; do
    echo "array_both[${key}]=${array_both[${key}]}"
done

Дава следната грешка:

./associative_arrays.sh: ред 3: array_both: true: трябва да използва долен индекс, когато присвоява асоциативен масив

Следното е решение, което измислих:

declare -Ar array1=( [5]=true [10]=true [15]=true )
declare -Ar array2=( [20]=true [25]=true [30]=true )
declare -A array_both=()

for key in ${!array1[@]}; do
    array_both+=( [${key}]=${array1[${key}]} )
done

for key in ${!array2[@]}; do
    array_both+=( [${key}]=${array2[${key}]} )
done

declare -r array_both

for key in ${!array_both[@]}; do
    echo "array_both[${key}]=${array_both[${key}]}"
done

Но се надявах, че всъщност ми липсва някаква граматика, която ще позволи присвояването на един ред, както е показано в неработещия пример.

Благодаря!


person Benjamin Leinweber    schedule 22.04.2015    source източник
comment
Един ред би изисквал възможност за разширяване на масив в [key]=value елемента за всеки ключ. Не знам за такова разширение. Най-близкото, за което се сещам, е това, което declare -p ви дава (което трябва да масажирате, за да използвате).   -  person Etan Reisner    schedule 22.04.2015
comment
Е, прекарах доста време в игра с масиви, разширение на параметри и bash променливи. Мисля, че е безопасно да се каже, че решението във вашия въпрос е най-чистият начин за копиране на асоциативен масив. Все пак мога да направя скрипта ви в едноредов с няколко точки и запетая, ако наистина искате... ;)   -  person vastlysuperiorman    schedule 23.04.2015
comment
ето какво направих: stackoverflow.com/a/38795114/526664   -  person Jason    schedule 29.07.2020


Отговори (5)


Аз също нямам едноредов, но тук има различно „заобиколно решение“, което някой може да хареса, използвайки преобразуване на низове. Това е 4 реда, така че съм само на 3 точки и запетая от отговора, който искахте!

declare -Ar array1=( [5]=true [10]=true [15]=true )
declare -Ar array2=( [20]=true [25]=true [30]=true )

# convert associative arrays to string
a1="$(declare -p array1)"
a2="$(declare -p array2)"

#combine the two strings trimming where necessary 
array_both_string="${a1:0:${#a1}-3} ${a2:21}"

# create new associative array from string
eval "declare -A array_both="${array_both_string#*=}

# show array definition
for key in ${!array_both[@]}; do
    echo "array_both[${key}]=${array_both[${key}]}"
done
person sprague44    schedule 23.04.2015
comment
Да, това със сигурност е алтернативно решение, което се основава на коментара на Etan. Не е точно интуитивно разглеждане на кода обаче, така че вероятно ще се придържам към моето решение. - person Benjamin Leinweber; 23.04.2015

Какво ще кажете за конкатениране на изхода от 'declare -p' за масивите (няма причина да не работи и за 'n' начин, показан тук):

#! /bin/bash

declare -Ar array1=(  [5]=true [10]=true [15]=true )
declare -Ar array2=( [20]=true [25]=true [30]=true )
declare -Ar array3=( [35]=true [40]=true [45]=true )

# one liner:
eval declare -Ar array_both=($(declare -p array1 array2 array3 | sed -z -e $'s/declare[^(]*(//g' -e $'s/)[^ ]//g'))

# proof:
for k in ${!array_both[$*]} ; do
  echo array_both[$k]=${array_both[$k}
done


person jafo    schedule 24.10.2019

Основната причина, поради която вторият ви опит не работи, е, че се опитвате да разрешите различен проблем, като използвате същото решение.

В първия набор от данни имате два числови индексирани масива, където ключовете нямат друго значение освен вероятно реда, в който се появяват, и техните стойности са това, което наистина има значение. Тълкувах, че това означава, че искате линейно да свържете тези стойности към нов масив с нов индекс, който отхвърля предишните ключове, но поддържа първоначалния ред на елементите, както и реда, в който сте ги предали.

Вторият набор от данни има два асоциативни индексирани масива, където ключовете са стойностите, а стойностите са просто контейнери. Забелязах, че сте използвали цифрови ключове, които, ако изберете да продължите да използвате числови индексирани масиви, ще ви позволят да запазите както реда на стойностите, така и реда на ключовете, при предположението, че искате ключовете във възходящ ред...

Така че за решаването на тези проблеми имам 3 удобни функции, които съм написал, които използват declare и eval за ускоряване на присъединяването/сливането на големи масиви, вместо да използват цикли за присвояване на всеки. Те също така приемат променлив брой масиви като аргумент, така че можете да се присъедините/слеете/изхвърлите колкото искате от тях.

ЗАБЕЛЕЖКА: Промених стойността/ключа "30" на "30 30", за да покажа как един низ би се държал различно от число при някои обстоятелства.

join_arrays(){
# <array> [<array> ...] <destination array>
# linear concatenates the values, re-keys the result.
# works best with indexed arrays where order is important but index value is not.
  local A_;
  while (( $# > 1 )); do
    A_+="\"\${$1[@]}\" ";
    shift;
  done
  eval "$1=($A_)";
}
# This works by building and running an array assignment command
# join_array a1 a2 a3 becomes a3=("${a1[@]" "$a2[@]" ); 

merge_arrays(){
# <array> [<array> ...] <destination array>
# merges the values, preserves the keys.
# works best with assoc arrays or to obtain union-like results.
# if a key exists in more than one array the latter shall prevail.

  local A_ B_;
  while (( $# > 1 )); do
    B_=`declare -p $1`;
    B_=${B_#*=??};
    A_+=${B_::-2}" ";
    shift;
  done
  eval "$1=($A_)";
}
# this crops the output of declare -p for each array
# then joining them into a single large assignment.
# try putting "echo" in front of the eval to see the result.


dump_arrays(){
# <array> [<array> ...]
# dumps array nodes in bash array subscript assignment format
# handy for use with array assignment operator.  Preseves keys.
# output is a join, but if you assign it you obtain a merge.

  local B_;
  while (( $# > 0 )); do
    B_=`declare -p $1`;
    B_=${B_#*=??};
    printf "%s " "${B_::-2}";
    shift;
  done
}
# same as above but prints it instead of performing the assignment


# The data sets, first the pair of indexed arrays:
declare -a array1=( 5 10 15 );
declare -a array2=( 20 25 "30 30" );
# then the set of assoc arrays:
declare -a array3=( [5]=true [10]=true [15]=true );
declare -a array4=( [20]=true [25]=true ["30 30"]=true );

# show them:
declare -p array1 array2 array3 array4;

# an indexed array for joins and an assoc array for merges:
declare -a joined;
declare -A merged;

# the common way to join 2 indexed arrays' values:
echo "joining array1+array2 using array expansion/assignment:";
joined=( "${array1[@]}" "${array2[@]}" );
declare -p joined;

declare -a joined='([0]="5" [1]="10" [2]="15" [3]="20" [4]="25" [5]="30 30" )'

# this does exactly the same thing, mostly saves me from typos ;-)
echo "joining array1+array2 using join_array():";
join_arrays array1 array2 joined;
declare -p joined;

declare -a joined='([0]="5" [1]="10" [2]="15" [3]="20" [4]="25" [5]="30 30" )'

# this merges them by key, which is inapropriate for this data set
# But I've included it for completeness to contrast join/merge operations
echo "merging array1+array2 using merge_array():";
merge_arrays array1 array2 merged;
declare -p merged;

declare -A merged='([0]="20" [1]="25" [2]="30 30" )'

# Example of joining 2 associative arrays:
# this is the usual way to join arrays but fails because
# the data is in the keys, not the values.
echo "joining array3+array4 using array expansion/assignment:"
joined=( "${array3[@]}" "${array4[@]}" );
declare -p joined;

declare -a joined='([0]="true" [1]="true" [2]="true" [3]="true" [4]="true" [5]="true") '

# and again, a join isn't what we want here, just for completeness.
echo "joining array3+array4 using join_array():";
join_arrays array3 array4 joined;
declare -p joined;

declare -a joined='([0]="true" [1]="true" [2]="true" [3]="true" [4]="true" [5]="true") '

# NOW a merge is appropriate, because we want the keys!
echo "merging array3+array4 using merge_array():"
merge_arrays array3 array4 merged;
declare -p merged;

declare -A merged='([25]="true" [20]="true" ["30 30"]="true" [10]="true" [15]="true" [5]=" вярно" )'

# Bonus points - another easy way to merge arrays (assoc or indexed) by key

# Note: this will only work if the keys are numeric... 
join_arrays array1 array2 joined;
# error expected because one keys is "30 30" ...
eval joined+=(`dump_arrays merged`);

bash: 30 30: синтактична грешка в израз (токенът за грешка е "30")

declare -p joined

declare -a joined='([0]="5" [1]="10" [2]="15" [3]="20" [4]="25" [5]="30 30" [20]="вярно" [25]="вярно")'

# Note: assoc arrays will not be sorted, even if keys are numeric!
join_arrays array1 array2 joined;
eval merged+=(`dump_arrays joined`);
declare -p merged

declare -A merged='([25]="true" [20]="true" ["30 30"]="true" [10]="true" [15]="true" [0]=" 5" [1]="10" [2]="15" [3]="20" [4]="25" [5]="true30 30" )'

Последна забележка: по-горе можете да видите, че ключ [5] има стойностите на ключа [5] на двата изходни масива, свързани, защото използвах оператора +=. Ако го използвате само за обединяване на списъци с флагове, това е безопасно, но за обединяване на списъци със значими стойности с възможни сблъсъци на ключове е по-добре да се придържате към функцията merge_array().

person Wil    schedule 20.03.2018

Въпреки че тази тема е стара, открих, че е супер полезен въпрос с проницателни отговори. Ето подобен подход на това, което @Wil обясни.

Подобно на този подход, този не използва външни команди (като sed).

Основната разлика е, че прави сливане, базирано на масив, вместо базирано на низ. Това позволява ключ-стойностите да бъдат заменени по предвидим начин. Той също така поддържа присвояване на обединения масив на променлива само за четене, както е показано във въпроса.

merge_map()
{
    local -A merged_array
    local array_string
    while [ $# -gt 0 ]
    do
        array_string=$(declare -p $1)
        eval merged_array+=${array_string#*=}
        shift
    done
    array_string=$(declare -p merged_array)
    echo "${array_string#*=}"
}

echo -e "\nExample from question..."

# Values in posted question
declare -Ar array1=( [5]=true [10]=true [15]=true )
declare -Ar array2=( [20]=true [25]=true [30]=true )
eval declare -Ar array_both=$(merge_map array1 array2)

# Show result
for k in "${!array_both[@]}";{ echo "[$k]=${array_both[$k]}";}

echo -e "\nExpanded example..."

# Non-numeric keys; some keys and values have spaces; more than two maps
declare -Ar expansion1=( [five]=true [ten]=true [and fifteen]=true )
declare -Ar expansion2=( [20]="true or false" [not 25]="neither true nor false" [30]=true )
declare -Ar expansion3=( [30]="was true, now false" [101]=puppies)
eval declare -Ar expansion_all=$(merge_map expansion1 expansion2 expansion3)

# Show result
for k in "${!expansion_all[@]}";{ echo "[$k]=${expansion_all[$k]}";}

person soith    schedule 03.05.2021

person    schedule
comment
Бихте ли дали също някакво обяснение? - person d4Rk; 24.06.2015