Как объединить ассоциативные массивы в 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
Да, это, безусловно, альтернативный обходной путь, основанный на комментарии Итана. Однако это не совсем интуитивно понятно, поэтому я, вероятно, буду придерживаться своего обходного пути. - 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;

объявить -ajoin='([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;

объявить -ajoin='([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;

объявить -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;

объявить -ajoin='([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;

объявить -ajoin='([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;

объявить -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

объявить -ajoin='([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

объявить -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