Ассоциативные массивы в сценариях оболочки

Нам понадобился script, который имитирует ассоциативные массивы или карту как структуру данных для Shell Scripting, любое тело?

Ответ 1

Чтобы добавить к Irfan answer, это более короткая и быстрая версия get(), поскольку для содержимого карты не требуется итерации:

get() {
    mapName=$1; key=$2

    map=${!mapName}
    value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}

Ответ 2

Другой вариант, если переносимость не является вашей основной задачей, заключается в использовании ассоциативных массивов, встроенных в оболочку. Это должно работать в bash 4.0 (доступно сейчас на большинстве основных дистрибутивов, но не на OS X, если вы не установите его самостоятельно), ksh и zsh:

declare -A newmap
newmap[name]="Irfan Zulfiqar"
newmap[designation]=SSE
newmap[company]="My Own Company"

echo ${newmap[company]}
echo ${newmap[name]}

В зависимости от оболочки вам может понадобиться typeset -A newmap вместо declare -A newmap, или в некоторых случаях это может вообще не понадобиться.

Ответ 3

Другой способ не bash 4.

#!/bin/bash

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

for animal in "${ARRAY[@]}" ; do
    KEY=${animal%%:*}
    VALUE=${animal#*:}
    printf "%s likes to %s.\n" "$KEY" "$VALUE"
done

echo -e "${ARRAY[1]%%:*} is an extinct animal which likes to ${ARRAY[1]#*:}\n"

Вы также можете указать оператор if для поиска. если [[$ var = ~/blah/]]. или что-то еще.

Ответ 4

Я думаю, что вам нужно отступить и подумать о том, что такое карта или ассоциативный массив. Все это способ сохранить значение для данного ключа и быстро и эффективно получить это значение. Вы также можете захотеть выполнить итерацию по ключам для получения каждой пары значений ключа или удалить ключи и связанные с ними значения.

Теперь подумайте о структуре данных, которую вы используете все время в сценариях оболочки, и даже просто в оболочке без записи script, которая имеет эти свойства. Тупик? Это файловая система.

Действительно, все, что вам нужно иметь ассоциативный массив в программировании оболочки, - это временная директория. mktemp -d - ваш конструктор ассоциативного массива:

prefix=$(basename -- "$0")
map=$(mktemp -dt ${prefix})
echo >${map}/key somevalue
value=$(cat ${map}/key)

Если вам не нравится использовать echo и cat, вы всегда можете написать несколько маленьких оберток; эти модели моделируются Irfan's, хотя они просто выводят значение, а не устанавливают произвольные переменные типа $value:

#!/bin/sh

prefix=$(basename -- "$0")
mapdir=$(mktemp -dt ${prefix})
trap 'rm -r ${mapdir}' EXIT

put() {
  [ "$#" != 3 ] && exit 1
  mapname=$1; key=$2; value=$3
  [ -d "${mapdir}/${mapname}" ] || mkdir "${mapdir}/${mapname}"
  echo $value >"${mapdir}/${mapname}/${key}"
}

get() {
  [ "$#" != 2 ] && exit 1
  mapname=$1; key=$2
  cat "${mapdir}/${mapname}/${key}"
}

put "newMap" "name" "Irfan Zulfiqar"
put "newMap" "designation" "SSE"
put "newMap" "company" "My Own Company"

value=$(get "newMap" "company")
echo $value

value=$(get "newMap" "name")
echo $value

edit. Этот подход на самом деле довольно немного быстрее, чем линейный поиск с использованием sed, предложенный исследователем, а также более надежный (он позволяет ключам и значениям содержать -, =, пробел, qnd ": SP:" ). Тот факт, что он использует файловую систему, не замедляет работу; эти файлы на самом деле никогда не гарантируются для записи на диск, если вы не вызываете sync; для временных файлов, подобных этому с коротким сроком службы, маловероятно, что многие из них никогда не будут записаны на диск.

Я сделал несколько тестов кода Ирфана, модификацию кода Ирфана в Jerry и мой код, используя следующую программу драйверов:

#!/bin/sh

mapimpl=$1
numkeys=$2
numvals=$3

. ./${mapimpl}.sh    #/ <- fix broken Qaru syntax highlighting

for (( i = 0 ; $i < $numkeys ; i += 1 ))
do
    for (( j = 0 ; $j < $numvals ; j += 1 ))
    do
        put "newMap" "key$i" "value$j"
        get "newMap" "key$i"
    done
done

Результаты:

    $ time ./driver.sh irfan 10 5

    real    0m0.975s
    user    0m0.280s
    sys     0m0.691s

    $ time ./driver.sh brian 10 5

    real    0m0.226s
    user    0m0.057s
    sys     0m0.123s

    $ time ./driver.sh jerry 10 5

    real    0m0.706s
    user    0m0.228s
    sys     0m0.530s

    $ time ./driver.sh irfan 100 5

    real    0m10.633s
    user    0m4.366s
    sys     0m7.127s

    $ time ./driver.sh brian 100 5

    real    0m1.682s
    user    0m0.546s
    sys     0m1.082s

    $ time ./driver.sh jerry 100 5

    real    0m9.315s
    user    0m4.565s
    sys     0m5.446s

    $ time ./driver.sh irfan 10 500

    real    1m46.197s
    user    0m44.869s
    sys     1m12.282s

    $ time ./driver.sh brian 10 500

    real    0m16.003s
    user    0m5.135s
    sys     0m10.396s

    $ time ./driver.sh jerry 10 500

    real    1m24.414s
    user    0m39.696s
    sys     0m54.834s

    $ time ./driver.sh irfan 1000 5

    real    4m25.145s
    user    3m17.286s
    sys     1m21.490s

    $ time ./driver.sh brian 1000 5

    real    0m19.442s
    user    0m5.287s
    sys     0m10.751s

    $ time ./driver.sh jerry 1000 5

    real    5m29.136s
    user    4m48.926s
    sys     0m59.336s

Ответ 5

hput () {
  eval hash"$1"='$2'
}

hget () {
  eval echo '${hash'"$1"'#hash}'
}
hput France Paris
hput Netherlands Amsterdam
hput Spain Madrid
echo `hget France` and `hget Netherlands` and `hget Spain`

$ sh hash.sh
Paris and Amsterdam and Madrid

Ответ 6

####################################################################
# Bash v3 does not support associative arrays
# and we cannot use ksh since all generic scripts are on bash
# Usage: map_put map_name key value
#
function map_put
{
    alias "${1}$2"="$3"
}

# map_get map_name key
# @return value
#
function map_get
{
    alias "${1}$2" | awk -F"'" '{ print $2; }'
}

# map_keys map_name 
# @return map keys
#
function map_keys
{
    alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }'
}

Пример:

mapName=$(basename $0)_map_
map_put $mapName "name" "Irfan Zulfiqar"
map_put $mapName "designation" "SSE"

for key in $(map_keys $mapName)
do
    echo "$key = $(map_get $mapName $key)
done

Ответ 7

Bash4 поддерживает это изначально. Не используйте grep или eval, они самые уродливые из хаков.

Для подробного подробного ответа с примером кода см. fooobar.com/questions/20275/...

Ответ 8

Теперь отвечая на этот вопрос.

Следующие скрипты имитируют ассоциативные массивы в сценариях оболочки. Его просто и очень легко понять.

Карта - это не что иное, как бесконечная строка, в которой keyValuePair сохраняется как --name = Irfan --designation = SSE --company = My: SP: Собственный: SP: Компания

пробелы заменяются на:: SP: для значений

put() {
    if [ "$#" != 3 ]; then exit 1; fi
    mapName=$1; key=$2; value=`echo $3 | sed -e "s/ /:SP:/g"`
    eval map="\"\$$mapName\""
    map="`echo "$map" | sed -e "s/--$key=[^ ]*//g"` --$key=$value"
    eval $mapName="\"$map\""
}

get() {
    mapName=$1; key=$2; valueFound="false"

    eval map=\$$mapName

    for keyValuePair in ${map};
    do
        case "$keyValuePair" in
            --$key=*) value=`echo "$keyValuePair" | sed -e 's/^[^=]*=//'`
                      valueFound="true"
        esac
        if [ "$valueFound" == "true" ]; then break; fi
    done
    value=`echo $value | sed -e "s/:SP:/ /g"`
}

put "newMap" "name" "Irfan Zulfiqar"
put "newMap" "designation" "SSE"
put "newMap" "company" "My Own Company"

get "newMap" "company"
echo $value

get "newMap" "name"
echo $value

edit: Просто добавлен другой метод для извлечения всех ключей.

getKeySet() {
    if [ "$#" != 1 ]; 
    then 
        exit 1; 
    fi

    mapName=$1; 

    eval map="\"\$$mapName\""

    keySet=`
           echo $map | 
           sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g"
          `
}

Ответ 9

Для Bash 3 существует частный случай, который имеет приятное и простое решение:

Если вы не хотите обрабатывать множество переменных, или ключи - это просто недопустимые идентификаторы переменных, и ваш массив должен иметь менее 256 элементов, вы можете злоупотреблять возвращаемыми значениями функции. Это решение не требует какой-либо подоболочки, поскольку это значение легко доступно как переменная или любая итерация, чтобы кричать производительность. Также он очень читается, почти как версия Bash 4.

Здесь самая базовая версия:

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[$?]}

Помните, используйте одинарные кавычки в case, иначе он будет подвержен глобализации. Действительно полезен для статических/замороженных хэшей с самого начала, но можно написать генератор индекса из массива hash_keys=().

Остерегайтесь, по умолчанию используется первый, поэтому вы можете выделить нулевой элемент:

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

hash_vals=("",           # sort of like returning null/nil for a non existent key
           "foo_val"
           "bar_val"
           "baz_val");

hash_index "foo" || echo ${hash_vals[$?]}  # It can't get more readable than this

Предостережение: длина неверна.

В качестве альтернативы, если вы хотите сохранить индексирование с нулевым индексом, вы можете зарезервировать другое значение индекса и защитить от несуществующего ключа, но это менее читаемо:

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

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

hash_index "foo"
[[ $? -ne 255 ]] && echo ${hash_vals[$?]}

Или, чтобы сохранить правильную длину, сдвинуть индекс на единицу:

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

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

hash_index "foo" || echo ${hash_vals[$(($? - 1))]}

Ответ 10

Вы можете использовать имена динамических переменных и позволить именам переменных работать как ключи хэш-карты.

Например, если у вас есть входной файл с двумя столбцами, именем, кредитом, как показано ниже, и вы хотите суммировать доход каждого пользователя:

Mary 100
John 200
Mary 50
John 300
Paul 100
Paul 400
David 100

Команда ниже будет суммировать все, используя динамические переменные в виде ключей, в виде карты _ $ {person}:

while read -r person money; ((map_$person+=$money)); done < <(cat INCOME_REPORT.log)

Чтобы прочитать результаты:

set | grep map

Выход будет:

map_David=100
map_John=500
map_Mary=150
map_Paul=500

Разрабатывая эти методы, я разрабатываю на GitHub функцию, которая работает так же, как объект HashMap, shell_map.

Чтобы создать "экземпляры HashMap", функция shell_map может создавать копии себя под разными именами. Каждая новая копия функции будет иметь другую переменную $ FUNCNAME. $ FUNCNAME затем используется для создания пространства имен для каждого экземпляра карты.

Ключами карты являются глобальные переменные, в виде $ FUNCNAME_DATA_ $ KEY, где ключ $ KEY - это ключ, добавленный к карте. Эти переменные являются динамическими переменными.

Bellow Я поставлю упрощенную версию, чтобы вы могли использовать ее в качестве примера.

#!/bin/bash

shell_map () {
    local METHOD="$1"

    case $METHOD in
    new)
        local NEW_MAP="$2"

        # loads shell_map function declaration
        test -n "$(declare -f shell_map)" || return

        # declares in the Global Scope a copy of shell_map, under a new name.
        eval "${_/shell_map/$2}"
    ;;
    put)
        local KEY="$2"  
        local VALUE="$3"

        # declares a variable in the global scope
        eval ${FUNCNAME}_DATA_${KEY}='$VALUE'
    ;;
    get)
        local KEY="$2"
        local VALUE="${FUNCNAME}_DATA_${KEY}"
        echo "${!VALUE}"
    ;;
    keys)
        declare | grep -Po "(?<=${FUNCNAME}_DATA_)\w+((?=\=))"
    ;;
    name)
        echo $FUNCNAME
    ;;
    contains_key)
        local KEY="$2"
        compgen -v ${FUNCNAME}_DATA_${KEY} > /dev/null && return 0 || return 1
    ;;
    clear_all)
        while read var; do  
            unset $var
        done < <(compgen -v ${FUNCNAME}_DATA_)
    ;;
    remove)
        local KEY="$2"
        unset ${FUNCNAME}_DATA_${KEY}
    ;;
    size)
        compgen -v ${FUNCNAME}_DATA_${KEY} | wc -l
    ;;
    *)
        echo "unsupported operation '$1'."
        return 1
    ;;
    esac
}

Применение:

shell_map new credit
credit put Mary 100
credit put John 200
for customer in 'credit keys'; do 
    value='credit get $customer'       
    echo "customer $customer has $value"
done
credit contains_key "Mary" && echo "Mary has credit!"

Ответ 11

Я нашел, что это правда, как уже упоминалось, лучший способ - записать ключ /vals в файл, а затем использовать grep/awk для их извлечения. Это звучит как всевозможные ненужные функции ввода-вывода, но кеш диска срабатывает и делает его чрезвычайно эффективным - намного быстрее, чем пытаться сохранить их в памяти, используя один из вышеуказанных методов (как показывают тесты).

Вот быстрый, чистый метод, который мне нравится:

hinit() {
    rm -f /tmp/hashmap.$1
}

hput() {
    echo "$2 $3" >> /tmp/hashmap.$1
}

hget() {
    grep "^$2 " /tmp/hashmap.$1 | awk '{ print $2 };'
}

hinit capitols
hput capitols France Paris
hput capitols Netherlands Amsterdam
hput capitols Spain Madrid

echo `hget capitols France` and `hget capitols Netherlands` and `hget capitols Spain`

Если вы хотите обеспечить одно значение для каждого ключа, вы также можете сделать небольшое действие grep/sed в hput().

Ответ 12

Как жаль, что я не видел вопроса раньше - я написал библиотеку shell-framework, которая содержит среди других карты (Ассоциативные массивы). Последнюю версию можно найти здесь здесь.

Пример:

#!/bin/bash 
#include map library
shF_PATH_TO_LIB="/usr/lib/shell-framework"
source "${shF_PATH_TO_LIB}/map"

#simple example get/put
putMapValue "mapName" "mapKey1" "map Value 2"
echo "mapName[mapKey1]: $(getMapValue "mapName" "mapKey1")"

#redefine old value to new
putMapValue "mapName" "mapKey1" "map Value 1"
echo "after change mapName[mapKey1]: $(getMapValue "mapName" "mapKey1")"

#add two new pairs key/values and print all keys
putMapValue "mapName" "mapKey2" "map Value 2"
putMapValue "mapName" "mapKey3" "map Value 3"
echo -e "mapName keys are \n$(getMapKeys "mapName")"

#create new map
putMapValue "subMapName" "subMapKey1" "sub map Value 1"
putMapValue "subMapName" "subMapKey2" "sub map Value 2"

#and put it in mapName under key "mapKey4"
putMapValue "mapName" "mapKey4" "subMapName"

#check if under two key were placed maps
echo "is map mapName[mapKey3]? - $(if isMap "$(getMapValue "mapName" "mapKey3")" ; then echo Yes; else echo No; fi)"
echo "is map mapName[mapKey4]? - $(if isMap "$(getMapValue "mapName" "mapKey4")" ; then echo Yes; else echo No; fi)"

#print map with sub maps
printf "%s\n" "$(mapToString "mapName")"

Ответ 13

В оболочке нет встроенной карты, такой как структура данных, я использую необработанную строку для описания таких элементов:

ARRAY=(
    "item_A|attr1|attr2|attr3"
    "item_B|attr1|attr2|attr3"
    "..."
)

при извлечении элементов и их атрибутов:

for item in "${ARRAY[@]}"
do
    item_name=$(echo "${item}"|awk -F "|" '{print $1}')
    item_attr1=$(echo "${item}"|awk -F "|" '{print $2}')
    item_attr2=$(echo "${item}"|awk -F "|" '{print $3}')

    echo "${item_name}"
    echo "${item_attr1}"
    echo "${item_attr2}"
done

Это кажется не умным, чем другие люди отвечают, но легко понять для новых людей для оболочки.

Ответ 14

Вы не знали, какой язык оболочки вы можете использовать, поэтому, возможно, вы можете рассмотреть AWK, в который встроены ассоциативные массивы.

Ответ 15

Я модифицировал решение Вадима со следующим:

####################################################################
# Bash v3 does not support associative arrays
# and we cannot use ksh since all generic scripts are on bash
# Usage: map_put map_name key value
#
function map_put
{
    alias "${1}$2"="$3"
}

# map_get map_name key
# @return value
#
function map_get {
    if type -p "${1}$2"
        then
            alias "${1}$2" | awk -F "'" '{ print $2; }';
    fi
}

# map_keys map_name 
# @return map keys
#
function map_keys
{
    alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }'
}

Изменение на map_get, чтобы предотвратить его возврат ошибок, если вы запрашиваете ключ, который не существует, хотя побочный эффект заключается в том, что он также молча игнорирует отсутствующие карты, но он лучше подходит для моего прецедента так как я просто хотел проверить ключ, чтобы пропустить элементы в цикле.

Ответ 16

несколько лет назад я написал библиотеку script для bash, которая поддерживала ассоциативные массивы среди других функций (протоколирование, файлы конфигурации, расширенную поддержку аргумента командной строки, генерировать справку, модульное тестирование и т.д.). Библиотека содержит оболочку для ассоциативных массивов и автоматически переключается на соответствующую модель (внутренняя для bash4 и эмуляция для предыдущих версий). Он назывался shell-framework и размещен на сайте origo.ethz.ch, но сегодня ресурс закрыт. Если кому-то все еще нужно, я могу поделиться им с вами.

Ответ 17

Поздний ответ, но рассмотрим проблему таким образом, используя bash builtin read, как показано в фрагменте кода из следующего брандмауэра ufw script. Преимущество такого подхода заключается в использовании как можно большего количества наборов полей (не только 2). Мы использовали разделитель |, поскольку для спецификаторов диапазона порта может потребоваться двоеточие, то есть 6001: 6010.

#!/usr/bin/env bash

readonly connections=(       
                            '192.168.1.4/24|tcp|22'
                            '192.168.1.4/24|tcp|53'
                            '192.168.1.4/24|tcp|80'
                            '192.168.1.4/24|tcp|139'
                            '192.168.1.4/24|tcp|443'
                            '192.168.1.4/24|tcp|445'
                            '192.168.1.4/24|tcp|631'
                            '192.168.1.4/24|tcp|5901'
                            '192.168.1.4/24|tcp|6566'
)

function set_connections(){
    local range proto port
    for fields in ${connections[@]}
    do
            IFS=$'|' read -r range proto port <<< "$fields"
            ufw allow from "$range" proto "$proto" to any port "$port"
    done
}

set_connections

Ответ 18

Добавление другой опции, если jq доступен:

export NAMES="{
  \"Mary\":\"100\",
  \"John\":\"200\",
  \"Mary\":\"50\",
  \"John\":\"300\",
  \"Paul\":\"100\",
  \"Paul\":\"400\",
  \"David\":\"100\"
}"
export NAME=David
echo $NAMES | jq --arg v "$NAME" '.[$v]' | tr -d '"'