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

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

+98
27 мар. '09 в 7:37
источник поделиться
18 ответов

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

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

    map=${!mapName}
    value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}
+20
27 мар. '09 в 16:48
источник

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

+120
27 мар. '09 в 19:22
источник

Другой способ не 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/]]. или что-то еще.

+83
14 дек. '10 в 22:14
источник

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

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

+31
27 мар. '09 в 18:46
источник
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
+13
30 сент. '09 в 0:12
источник
####################################################################
# 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
28 июн. '11 в 15:28
источник

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

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

+5
12 авг. '10 в 13:20
источник

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

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

Карта - это не что иное, как бесконечная строка, в которой 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"
          `
}
+3
27 мар. '09 в 14:07
источник

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

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

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!"
+2
03 июн. '16 в 8:17
источник

Для 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))]}
+2
03 мар. '14 в 16:15
источник

Как жаль, что я не видел вопроса раньше - я написал библиотеку 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")"
+1
01 мар. '11 в 21:29
источник

Я нашел, что это правда, как уже упоминалось, лучший способ - записать ключ /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().

+1
09 февр. '10 в 17:19
источник

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

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

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

+1
05 авг. '15 в 8:38
источник

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

0
13 июн. '13 в 12:38
источник

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

0
29 сент. '09 в 19:49
источник

Поздний ответ, но рассмотрим проблему таким образом, используя 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
0
15 сент. '15 в 15:59
источник

Добавление другой опции, если 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 '"' 
0
13 июл. '18 в 20:11
источник

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

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

0
11 апр. '13 в 14:22
источник

Посмотрите другие вопросы по меткам или Задайте вопрос