Как определить хэш-таблицы в Bash?
что эквивалентно словари Python но в Bash (должен работать через OS X и Linux).
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, который должен быть везде, строку и регулярные выражения. Мне это особенно нравится, когда словарь не слишком большой.
- выберите 2 разделителя, которые вы не будете использовать в ключах и значениях (например,', ' и ':' )
запишите карту в виде строки (обратите внимание на разделитель ',' также в начале и конце)
animals=",moo:cow,woof:dog,"
использовать регулярное выражение для извлечения значения
get_animal { echo "$(expr "$animals" : ".*,:\([^,]*\),.*")" }
разделить строку, чтобы перечислить элементы
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.