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

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

470
29 сент. '09 в 21:29
источник поделиться
16 ответов

Баш 4

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

Вы объявляете ассоциативный массив, выполняя:

declare -A animals

Вы можете заполнить его элементами, используя обычный оператор присваивания массива. Например, если вы хотите иметь карту animal[sound(key)] = animal(value):

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

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

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

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

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

Баш 3

До bash 4 у вас не было ассоциативных массивов. Не используйте eval чтобы подражать им. Избегайте eval как чума, потому что это чума сценариев оболочки. Наиболее важной причиной является то, что 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=$1 index=$2
    local i="${array}_$index"
    printf '%s' "${!i}"
}

Позвольте использовать это:

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

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

Резюме:

  • Обновите систему до bash 4 и используйте declare -A для ассоциативных массивов.
  • Используйте опцию declare если вы не можете обновить.
  • Попробуйте вместо этого использовать awk и вообще избежать этой проблемы.
781
12 авг. '10 в 16:09
источник

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

#!/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 лучше, конечно, но если вам нужен взлом... только взломать будет. Вы можете искать массив/хэш с аналогичными методами.

103
15 дек. '10 в 1:02
источник

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

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" )
62
23 мая '11 в 3:30
источник

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

hput() {
    eval "$1""$2"='$3'
}

hget() {
    eval echo '${'"$1$2"'#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 "", чтобы получить их, используя каналы с cut или awk или sed или что-то другое, чтобы получить значения.

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

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

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

hput() {
    echo "$2 $3" >> /tmp/hashmap.$1
}

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

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`
23
09 февр. '10 в 2:38
источник

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

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

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

hashtable=$(mktemp -d)

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

echo $value > $hashtable/$key

Читать элемент

value=$(< $hashtable/$key)

Спектакль

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

15
06 дек. '16 в 18:36
источник
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
14
30 сент. '09 в 1:45
источник

Рассмотрим решение с использованием bash встроенного прочитанного, как показано в фрагменте кода из следующего брандмауэра 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
10
15 сент. '15 в 20:16
источник

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

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

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

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

    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
6
18 апр. '14 в 2:05
источник

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

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

hput() {
    printf "$3" > /tmp/hashmap.$1/$2
}

hget() {
    cat /tmp/hashmap.$1/$2
}

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

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

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

Просто подумал, что я смогу. Приветствия!

Изменить: добавление hdestroy()

5
28 окт. '10 в 21:36
источник

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=$2

  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=$2

  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[@])
2
29 авг. '13 в 18:26
источник

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

Ранее я использовал cksum для хэширования, но с тех пор перевел Java-строку hashCode в нативный bash/zsh.

# Here the hashing function
ht() {
  local h=0 i
  for (( i=0; i < ${#1}; i++ )); do
    let "h=( (h<<5) - h ) + $(printf %d \'${1:$i:1})"
    let "h |= h"
  done
  printf "$h"
}

# 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
echo ${#myhash[@]} # "2" - there are two values (note, zsh doesn't count right)

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

2
18 окт. '12 в 3:39
источник

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

function hget {

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

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

Grep/Awk:

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

$ 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.

2
15 авг. '10 в 2:45
источник

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

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

2
12 авг. '10 в 15:53
источник

Я также использовал метод 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
0
04 сент. '15 в 9:31
источник

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

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

0
03 июня '16 в 19:34
источник

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

grep --max_count = 1... или grep -m 1...

0
01 апр. '10 в 18:47
источник

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