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



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

665   17  

17 ответов:

добавить ответ Ирфана, вот более короткая и быстрая версия get() так как он не требует итерации по содержимому карты:

get() {
    mapName=; key=

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

другой вариант, если переносимость не является вашей главной заботой, заключается в использовании ассоциативных массивов, встроенных в оболочку. Это должно работать в 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, или в некоторых это может вообще не понадобиться.

еще один способ не Баш 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/ ]]. или еще что-нибудь.

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

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

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

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

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

#!/bin/sh

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

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

get() {
  [ "$#" != 2 ] && exit 1
  mapname=; key=
  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, предложенный вопрошающим, а также более надежный (он позволяет ключам и значениям содержать -,=, space, qnd ":SP:"). Тот факт, что он использует файловую систему не делает его медленным; эти файлы на самом деле никогда не гарантируется запись на диск, если вы не вызываете sync; для временных файлов, как это с коротким сроком службы, это не маловероятно, что многие из них никогда не будет записан на диск.

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

#!/bin/sh

mapimpl=
numkeys=
numvals=

. ./${mapimpl}.sh    #/ <- fix broken stack overflow 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

hput () {
  eval hash""=''
}

hget () {
  eval echo '${hash'""'#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
####################################################################
# 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 ""=""
}

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

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

пример:

mapName=$(basename )_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

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

подробный ответ с примером кода см. В разделе: https://stackoverflow.com/questions/3467959

теперь ответить на этот вопрос.

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

карта-это не что иное, как бесконечная строка, в которой keyValuePair сохранена как --имя=Ирфан --обозначение=ГСП --компании=Мой:СП:собственный:СП:компания

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

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

get() {
    mapName=; key=; 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=; 

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

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

для Bash 3, есть частный случай, который имеет хорошее и простое решение:

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

вот самая основная версия:

hash_index() {
    case  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  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  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  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))]}

вы можете использовать динамические имена переменных и пусть имена переменных работают как ключи hashmap.

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

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

команда ниже будет суммировать все, используя динамические переменные в качестве ключей, в виде map_${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 " the функции shell_map умеет создавать копии себя под разными именами. Каждая новая копия функции будет иметь другую переменную $FUNCNAME. Затем $FUNCNAME используется для создания пространства имен для каждого экземпляра карты.

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

ниже я поставлю упрощенную версию этого, так что вы можете использовать в качестве примера.

#!/bin/bash

shell_map () {
    local METHOD=""

    case $METHOD in
    new)
        local NEW_MAP=""

        # 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/}"
    ;;
    put)
        local KEY=""  
        local VALUE=""

        # declares a variable in the global scope
        eval ${FUNCNAME}_DATA_${KEY}='$VALUE'
    ;;
    get)
        local KEY=""
        local VALUE="${FUNCNAME}_DATA_${KEY}"
        echo "${!VALUE}"
    ;;
    keys)
        declare | grep -Po "(?<=${FUNCNAME}_DATA_)\w+((?=\=))"
    ;;
    name)
        echo $FUNCNAME
    ;;
    contains_key)
        local KEY=""
        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=""
        unset ${FUNCNAME}_DATA_${KEY}
    ;;
    size)
        compgen -v ${FUNCNAME}_DATA_${KEY} | wc -l
    ;;
    *)
        echo "unsupported operation ''."
        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!"

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

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

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

hput() {
    echo " " >> /tmp/hashmap.
}

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

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().

Как жаль, что я не видел вопрос раньше-я написал библиотеку 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")"

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

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

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

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

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

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

Я изменил решение Вадима следующим образом:

####################################################################
# 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 ""=""
}

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

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

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

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

поздний ответ, но рассмотрите возможность решения проблемы таким образом, используя встроенный bash читать как показано в фрагменте кода из сценария брандмауэра ufw, который следует. Этот подход имеет преимущество использования столько наборов полей с разделителями (а не только 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

добавление другой опции, если 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 '"' 

Comments

    Ничего не найдено.