Вопрос

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

Это было полезно?

Решение

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

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

    map=${!mapName}
    value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -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, или в некоторых это может быть вообще не нужно.

Еще один способ, не связанный с 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 =~ /бла/ ]].или что там еще.

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

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

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

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

Если вам не хочется использовать echo и cat, вы всегда можете написать несколько маленьких оберток;эти модели смоделированы на основе Irfan, хотя они просто выводят значение, а не устанавливают произвольные переменные, такие как $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

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

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

#!/bin/sh

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

. ./${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"$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

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

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

####################################################################
# 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

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

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

Карта - это не что иное, как бесконечная строка, которая имеет KeyValuePair, сохраненную как --name=Irfan --обозначение=SSE --company= My:SP: Own:SP:Company

пробелы заменяются на ':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

Редактировать: Просто добавлен еще один метод для извлечения всех ключей.

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

    mapName=$1; 

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

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

Для 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))]}

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

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

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

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

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 - это ключ, добавленный к Карте.Этими переменными являются динамические переменные.

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

#!/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!"

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

Пример:

#!/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")"

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

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

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

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

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

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

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

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

Поздний ответ, но рассмотрите возможность решения проблемы таким образом, используя встроенный 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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top