Как определить хэш-таблицы в Bash?


что эквивалентно словари Python но в Bash (должен работать через OS X и Linux).

16 398

16 ответов:

Баш 4

Bash 4 изначально поддерживает эту функцию. Убедитесь, что hashbang вашего скрипта #!/usr/bin/env bash или #!/bin/bash или что-нибудь еще, что ссылки bash, а не sh. Убедитесь, что вы выполняете свой скрипт, а не делаете что-то глупое, как sh script что бы вызвать ваш bash hashbang следует игнорировать. Это элементарные вещи, но так много не получается, поэтому повторов.

вы объявляете ассоциативный массив делать:

declare -A animals

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

animals=( ["moo"]="cow" ["woof"]="dog")

или объединить их:

declare -A animals=( ["moo"]="cow" ["woof"]="dog")

затем использовать их как обычные массивы. "${animals[@]}" расширяет ценностей, "${!animals[@]}" (уведомление !) расширяет ключи. Не забудьте процитировать:

echo "${animals[moo]}"
for sound in "${!animals[@]}"; do echo "$sound - ${animals[$sound]}"; done

Баш 3

перед bash 4 у вас нет ассоциативных массивов. не используйте eval подражать им. Вы должны избегать эвал как чумы, потому что это и чума сценариев оболочки. Самая важная причина заключается в том, что вы не хотите рассматривать свои данные как исполняемый код (есть много других причин).

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

если у вас есть какое-то глупое оправдание, почему вы"не могу обновить",declare - это гораздо более безопасный вариант. Он не оценивает данные как код bash, например eval делает,и как таковой он не позволяет произвольную инъекцию кода довольно легко.

давайте подготовим ответ, введя понятия:

во-первых, косвенность (серьезно; никогда не используйте это, если вы не психически больны или не имеете другого плохого оправдания для написания хаки.)

$ animals_moo=cow; sound=moo; i="animals_$sound"; echo "${!i}"
cow

во-вторых, declare:

$ sound=moo; animal=cow; declare "animals_$sound=$animal"; echo "$animals_moo"
cow

свести их вместе:

# Set a value:
declare "array_$index=$value"

# Get a value:
arrayGet() { 
    local array= index=
    local i="${array}_$index"
    printf '%s' "${!i}"
}

давайте использовать его:

$ sound=moo
$ animal=cow
$ declare "animals_$sound=$animal"
$ arrayGet animals "$sound"
cow

Примечание: declare не может быть помещен в функцию. Любое использование declare внутри функции bash поворачивается переменная, которую она создает местные к области действия этой функции, что означает, что мы не можем получить доступ или изменить глобальные массивы с ним. (В bash 4 Вы можете использовать declare-g для объявления глобальных переменных , но в bash 4 Вы следует использовать ассоциативные массивы, в первую очередь, а не эту халтуру.)

резюме

обновление до bash 4 и использовать declare -A. Если вы не можете, подумайте о переходе полностью на awk прежде чем делать уродливые хаки, как описано выше. И определенно держитесь подальше от eval шаманства.

есть подстановка параметров, хотя это может быть и un-PC ...например, косвенность.

#!/bin/bash

# Array pretending to be a Pythonic dictionary
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

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

способ BASH 4 лучше, конечно, но если вам нужен Хак ...только рубить. Вы можете искать массив / хэш с аналогичными методами.

это то, что я искал здесь:

declare -A hashmap
hashmap["key"]="value"
hashmap["key2"]="value2"
echo "${hashmap["key"]}"
for key in ${!hashmap[@]}; do echo $key; done
for value in ${hashmap[@]}; do echo $value; done
echo hashmap has ${#hashmap[@]} elements

это не работает для меня с bash 4.1.5:

animals=( ["moo"]="cow" )

вы можете дополнительно изменить интерфейс hput()/hget (), чтобы вы назвали хэши следующим образом:

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

hget() {
    eval echo '${'""'#hash}'
}

а то

hput capitals France Paris
hput capitals Netherlands Amsterdam
hput capitals Spain Madrid
echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain`

Это позволяет определить другие карты, которые не конфликтуют (например, "rcapitals", который выполняет поиск страны по столице). Но, в любом случае, я думаю, вы обнаружите, что все это довольно ужасно, с точки зрения производительности.

Если вы действительно хотите быстрый поиск по хеш, это ужасный хак, который на самом деле работает очень хорошо. Это это: запишите свой ключ / значения во временный файл, по одному на строку, а затем используйте "grep" ^$key", чтобы получить их, используя трубы с разрезом или awk или sed или что-то еще для извлечения значений.

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

вот один из способов сделать это:

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

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

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

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

echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain`
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 builtin читать как показано в фрагменте кода из сценария брандмауэра 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

просто используйте файловую систему

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

создание хеш-таблицы

hashtable=$(mktemp -d)

добавить элемент

echo $value > $hashtable/$key

прочитать элемент

value=$(< $hashtable/$key)

производительность

конечно, это медленно, но не это медленно. Я тестировал его на своей машине, с SSD и btrfs, и это делает вокруг 3000 элементов чтения / записи в секунду.

Я согласен с @lhunath и другими, что ассоциативный массив-это способ пойти с Bash 4. Если вы застряли в Bash 3 (OSX, старые дистрибутивы, которые вы не можете обновить), вы можете использовать также expr, который должен быть везде, строку и регулярные выражения. Мне это особенно нравится, когда словарь не слишком большой.

  1. выберите 2 разделителя, которые вы не будете использовать в ключах и значениях (например,', ' и ':' )
  2. запишите карту в виде строки (обратите внимание на разделитель ',' также в начале и конце)

    animals=",moo:cow,woof:dog,"
    
  3. использовать регулярное выражение для извлечения значения

    get_animal {
        echo "$(expr "$animals" : ".*,:\([^,]*\),.*")"
    }
    
  4. разделить строку, чтобы перечислить элементы

    get_animal_items {
        arr=$(echo "${animals:1:${#animals}-2}" | tr "," "\n")
        for i in $arr
        do
            value="${i##*:}"
            key="${i%%:*}"
            echo "${value} likes to $key"
        done
    }
    

теперь вы можете использовать его:

$ animal = get_animal "moo"
cow
$ get_animal_items
cow likes to moo
dog likes to woof

мне очень понравился ответ Al P, но я хотел, чтобы уникальность применялась дешево, поэтому я сделал еще один шаг - использовал каталог. Есть некоторые очевидные ограничения (ограничения файлов каталогов, недопустимые имена файлов), но это должно работать в большинстве случаев.

hinit() {
    rm -rf /tmp/hashmap.
    mkdir -p /tmp/hashmap.
}

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

hget() {
    cat /tmp/hashmap./
}

hkeys() {
    ls -1 /tmp/hashmap.
}

hdestroy() {
    rm -rf /tmp/hashmap.
}

hinit ids

for (( i = 0; i < 10000; i++ )); do
    hput ids "key$i" "value$i"
done

for (( i = 0; i < 10000; i++ )); do
    printf '%s\n' $(hget ids "key$i") > /dev/null
done

hdestroy ids

Он также выполняет немного лучше в моих тестах.

$ time bash hash.sh 
real    0m46.500s
user    0m16.767s
sys     0m51.473s

$ time bash dirhash.sh 
real    0m35.875s
user    0m8.002s
sys     0m24.666s

просто подумал, что я вмешаюсь. Ура!

Edit: добавление hdestroy ()

две вещи, вы можете использовать память вместо /tmp в любом ядре 2.6 с помощью /dev/shm (Redhat) другие дистрибутивы могут отличаться. Также hget может быть переопределен с помощью следующим образом:

function hget {

  while read key idx
  do
    if [ $key =  ]
    then
      echo $idx
      return
    fi
  done < /dev/shm/hashmap.
}

кроме того, предполагая, что все ключи уникальны, возврат короткого замыкания цикла чтения и предотвращает необходимость считывания всех записей. Если ваша реализация может иметь дубликаты ключей,то просто оставьте возврат. Это экономит расходы на чтение и разветвление как grep, так и awk. С помощью /dev / shm для обеих реализаций дал следующее, используя time hget на хэше 3 записей, ищущем последнюю запись:

Grep / Awk:

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

$ time echo $(hget FD oracle)
3

real    0m0.011s
user    0m0.002s
sys     0m0.013s

читать/Эхо:

$ time echo $(hget FD oracle)
3

real    0m0.004s
user    0m0.000s
sys     0m0.004s

при нескольких вызовах я никогда не видел менее 50% улучшения. Все это можно отнести к вилке над головой, благодаря использованию /dev/shm.

Bash 3 решение:

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

# Define a hash like this
MYHASH=("firstName:Milan"
        "lastName:Adamovsky")

# Function to get value by key
getHashKey()
 {
  declare -a hash=("${!1}")
  local key
  local lookup=

  for key in "${hash[@]}" ; do
   KEY=${key%%:*}
   VALUE=${key#*:}
   if [[ $KEY == $lookup ]]
   then
    echo $VALUE
   fi
  done
 }

# Function to get a list of all keys
getHashKeys()
 {
  declare -a hash=("${!1}")
  local KEY
  local VALUE
  local key
  local lookup=

  for key in "${hash[@]}" ; do
   KEY=${key%%:*}
   VALUE=${key#*:}
   keys+="${KEY} "
  done

  echo $keys
 }

# Here we want to get the value of 'lastName'
echo $(getHashKey MYHASH[@] "lastName")


# Here we want to get all keys
echo $(getHashKeys MYHASH[@])

до bash 4 нет хорошего способа использовать ассоциативные массивы в bash. Лучше всего использовать интерпретируемый язык, который на самом деле поддерживает такие вещи, как awk. С другой стороны, bash 4 тут поддержать их.

Как меньше хорошие способы в bash 3, вот ссылка, чем может помочь:http://mywiki.wooledge.org/BashFAQ/006

коллега только что упомянул эту тему. Я самостоятельно реализованы хэш-таблицы в bash, и это не зависит от версии 4. Из моего блога в марте 2010 года (до некоторых ответов здесь...) под названием хэш-таблицы в bash:

# Here's the hashing function
ht() { local ht=`echo "$*" |cksum`; echo "${ht//[!0-9]}"; }

# Example:

myhash[`ht foo bar`]="a value"
myhash[`ht baz baf`]="b value"

echo ${myhash[`ht baz baf`]} # "b value"
echo ${myhash[@]} # "a value b value" though perhaps reversed

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

чтобы получить немного больше производительности помните, что grep имеет функцию stop, чтобы остановить, когда он находит N-е совпадение в этом случае n будет 1.

grep --max_count=1 ... или грэп -М 1 ...

Я также использовал способ bash4, но я нахожу и раздражает ошибка.

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

for instanceId in $instanceList
do
   aws cloudwatch describe-alarms --output json --alarm-name-prefix $instanceId| jq '.["MetricAlarms"][].StateValue'| xargs | grep -E 'ALARM|INSUFFICIENT_DATA'
   [ $? -eq 0 ] && statusCheck+=([$instanceId]="checkKO") || statusCheck+=([$instanceId]="allCheckOk"
done

я узнаю, что с bash 4.3.11 добавление к существующему ключу в dict привело к добавлению значения, если оно уже присутствует. Так, например, после некоторого повторения содержание значения было "checkKOcheckKOallCheckOK", и это было не хорошо.

нет проблем с bash 4.3.39, где добавление существующий ключ означает замену фактического значения, если оно уже присутствует.

Я решил это просто очистка / объявление ассоциативного массива statusCheck перед cicle:

unset statusCheck; declare -A statusCheck

Я создаю хэш-карты в bash 3 с помощью динамических переменных. Я объяснил, как это работает в моем ответе: ассоциативные массивы в Shell-скриптах

также вы можете посмотреть в shell_map, который является реализацией HashMap, выполненной в bash 3.