Як визначити хеш-таблиці в Bash?


Відповіді:


938

Баш 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трактує ваші дані як виконуваний код (є й багато інших причин).

Перше і головне : Подумайте про модернізацію до гри 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а не уникайте проблеми взагалі.

1
@ Richard: Імовірно, ви насправді не використовуєте bash. Ваш хешбанг sh замість bash, чи ви іншим чином посилаєте свій код на sh? Спробуйте поставити це прямо перед тим, як заявити: echo "$ BASH_VERSION $ POSIXLY_CORRECT", він повинен виводити, 4.xа не y.
lhunath

5
Неможливо оновити. Єдина причина, по якій я пишу сценарії в Bash, полягає в тому, щоб переносити "будь-де". Отже, покладаючись на неуніверсальну особливість Баша, цей підхід виходить. Що шкода, бо інакше це було б чудовим рішенням для мене!
Стів Пітчерс

3
Прикро, що OSX за замовчуванням застосовується до Bash 3, оскільки це є "за замовчуванням" для багатьох людей. Я подумав, що відлякування ShellShock, можливо, було необхідним поштовхом, але, очевидно, ні.
кен

13
@ken це проблема з ліцензуванням. Bash на OSX застряг в останній ліцензії, що не має ліцензії GPLv3.
lhunath

2
... або sudo port install bashдля тих (з розумом, IMHO), які не бажають робити каталоги в PATH для всіх користувачів, що підлягають запису, без явного ескалації привілеїв під час процесу.
Чарльз Даффі

125

Існує підміна параметрів, хоча це може бути і без ПК ... як непряма.

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


5
Я б змінив це, щоб VALUE=${animal#*:}захистити випадок, колиARRAY[$x]="caesar:come:see:conquer"
glenn jackman

2
Також корисно поставити подвійні лапки навколо $ {ARRAY [@]} на випадок, якщо в ключах або значеннях є пробіли, як вfor animal in "${ARRAY[@]}"; do
devguydavid

1
Але чи не дуже ефективність? Я думаю, що O (n * m), якщо ви хочете порівняти з іншим списком ключів, замість O (n) з правильними хешмапами (постійний пошук часу, O (1) для однієї клавіші).
CodeManX

1
Ідея менша про ефективність, більше про здатність розуміти / читати для тих, хто має фони в перлі, пітоні чи навіть баш 4. Дозволяє писати подібним чином.
Bubnoff

1
@CoDEmanX: це хакер , розумний і елегантний, але все ще рудиментарний спосіб вирішити проблему, щоб допомогти бідним душам, які все ще застрягли в 2007 році з Bash 3.x. У такому простому коді не можна очікувати "правильних хешмапів" або міркувань щодо ефективності.
MestreLion

85

Ось що я шукав тут:

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" )

2
Зауважте, що значення може не містити пробілів, інакше ви додасте відразу більше елементів
rubo77

6
Оновлення для синтаксису хешмапу ["key"] = "value", який я також виявив відсутнім у відповіді, інакше фантастично прийнятої відповіді.
thomanski

@ rubo77 ключ ні, він додає кілька клавіш. Будь-який спосіб вирішити це?
Xeverous

25

Ви можете додатково змінити інтерфейс 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 '', щоб отримати їх, використовуючи труби з вирізанням або awk або sed або будь-яким іншим для отримання значень.

Як я вже говорив, це звучить жахливо, і це здається, що це повинно бути повільним і робити всілякі непотрібні введення-виведення, але на практиці це дуже швидко (дисковий кеш-клас є дивним, чи не так?), Навіть для дуже великого хешу столи. Ви повинні самостійно застосувати унікальність ключів і т. Д. Навіть якщо у вас є лише кілька сотень записів, вихідний файл / grep комбо буде досить швидким - на мій досвід, у кілька разів швидшим. Він також їсть менше пам'яті.

Ось один із способів зробити це:

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`

1
Чудово! ви навіть можете це повторити: для i в $ (compgen -A змінні капітолі); do hget "$ i" "" зроблено
zhaorufei

22

Просто використовуйте файлову систему

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

Створення хештелів

hashtable=$(mktemp -d)

Додайте елемент

echo $value > $hashtable/$key

Прочитайте елемент

value=$(< $hashtable/$key)

Продуктивність

Звичайно, його повільно, але не , що повільно. Я перевірив це на своїй машині, з SSD та btrfs , і він робить близько 3000 елементів читання / запису за секунду .


1
Яка версія bash підтримує mkdir -d? (Не 4.3, на Ubuntu 14. Я б mkdir /run/shm/foomkdir /tmp/foo
вдався

1
Можливо, mktemp -dмалося на увазі замість цього?
Рейд Елліс

2
Цікаво, в чому різниця між $value=$(< $hashtable/$key)і value=$(< $hashtable/$key)? Дякую!
Хелін Ван

1
"перевірено на моїй машині" Це звучить як чудовий спосіб випалити отвір через ваш SSD. Не всі дистрибутиви Linux використовують tmpfs за замовчуванням.
kirbyfan64sos

Обробляю близько 50000 хешей. Perl і PHP роблять це волосся за пів секунди. Вузол за 1 секунду і щось. Опція FS звучить повільно. Однак чи можемо ми переконатися, що файли існують лише в оперативній пам'яті?
Рольф

14
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

31
Зітхніть, це здається образливим і все одно неточним. Не можна було б розміщувати валідацію, втечу чи кодування (див., Я фактично знаю) у кишках хеш-таблиці, а скоріше в обгортку та якнайшвидше після введення.
DigitalRoss

@DigitalRoss Ви можете пояснити, в чому полягає використання #hash в eval echo '$ {hash' "$ 1" '# хеш}' . для мене це здається мені коментарем не більше того. чи має тут #hash якесь особливе значення?
Санджай

@Sanjay ${var#start}видаляє текст старт з початку значення , що зберігається в змінної Var .
jpaugh

11

Розглянемо рішення, використовуючи 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

2
@CharlieMartin: читання - це дуже потужна функція і її недостатньо використовують багато програмістів. Це дозволяє компактні форми LISP типу обробки списку. Наприклад, у наведеному вище прикладі ми можемо зняти лише перший елемент і зберегти решту (тобто подібну концепцію до першого та відпочинок у lisp), виконавши:IFS=$'|' read -r first rest <<< "$fields"
AsymLabs

6

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

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

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

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
  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

5

Мені дуже сподобалась відповідь Аль П, але я хотів, щоб унікальність набула дешевого способу, тому я зробив це на крок далі - скористайтеся каталогом. Існують деякі очевидні обмеження (обмеження файлів каталогів, недійсні імена файлів), але це повинно працювати в більшості випадків.

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 ()


3

Дві речі, ви можете використовувати пам'ять замість / 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.


3

Співробітник щойно згадував цю нитку. Я самостійно реалізував хеш-таблиці в межах bash, і це не залежить від версії 4. З моєї публікації в блозі в березні 2010 року (до деяких відповідей тут ...) під назвою Hash таблиці in bash :

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

# Here's 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та друзів.


Посилання у відповіді страшно! Якщо натиснути на нього, ви застрягли в циклі перенаправлення. Будь ласка, оновіть.
Ракіб

1
@MohammadRakibAmin - Так, мій веб-сайт не працює, і я сумніваюся, що я відновлю свій блог. Я оновив вищезазначене посилання на архівну версію. Дякуємо за ваш інтерес!
Адам Кац

2

До bash 4 не існує хорошого способу використання асоціативних масивів у bash. Ваша найкраща ставка - використовувати інтерпретовану мову, яка насправді підтримує такі речі, як awk. З іншого боку, Баш 4 робить їх підтримки.

Що стосується менш хороших способів в bash 3, тут є посилання, ніж це може допомогти: http://mywiki.wooledge.org/BashFAQ/006


2

Рішення 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[@])

Я думаю, що це досить акуратний фрагмент. Це може використовувати трохи очищення (хоча не багато). У моїй версії я перейменував "ключ" на "пару" і зробив KEY та VALUE малі регістри (тому що я використовую великі регістри, коли експортуються змінні). Я також перейменував на getHashKey в getHashValue і зробив і ключовими, і значеннями місцевими (хоча іноді хотілося б, щоб вони не були локальними). У getHashKeys я не присвоюю нічого значення. Я використовую крапку з комою для розділення, оскільки мої значення - це URL-адреси.

0

Я також використовував 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 додавання до існуючого ключа в диктаті призвело до додавання значення, якщо воно вже є. Так, наприклад, після деякого повторення зміст значення було "checkKOcheckKOallCheckOK", і це було не добре.

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

Я вирішив це лише очищення / оголошення асоціативного масиву statusCheck перед циклом:

unset statusCheck; declare -A statusCheck

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.