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

436

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

  • 3
    Пусть bash запустит скрипт на python / perl ... Это так гибко!
  • 0
    Попробуйте использовать xonsh (это на github).
Теги:
dictionary
hashtable
associative-array

16 ответов

707
Лучший ответ

Баш 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['key']='value' для установки значения, "${animals[@]}" расширяет значения, "${!animals[@]}" (обратите внимание на !) расширяет ключи. Не забудьте процитировать их:

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

Баш 3

До bash 4 у вас не было ассоциативных массивов. Не используйте 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. Если вы не можете, подумайте о переходе на awk прежде чем делать некрасивые хаки, как описано выше. И, безусловно, остаться щеколдами от eval повозки, запряженных волов.

  • 0
    +1 за declare -A , я не могу поверить, что никогда не использовал это раньше! Я запрограммировал Bash на 10 лет.
  • 51
    Ах, я ценю прогнозные настроения, но «я не могу обновить сервер, за который я не отвечаю», вряд ли это глупое оправдание, почему вы не можете обновить. К сожалению, многим людям, у которых есть реальная работа, требуется выполнять ее на рабочем месте, принадлежащем кому-то другому, на серверах, управляемых кем-то другим, и если вы работаете в ИТ, я уверен, что вы знаете, как приятно пользователям прийти и требовать изменений в ИТ. Я знаю, что это печальное и трагическое положение вещей, но это жизнь!
Показать ещё 26 комментариев
102

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

#!/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"
  • 1
    Отлично!! Как раз то, что мне нужно и быстро. Спасибо
Показать ещё 6 комментариев
54

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

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" )
  • 1
    Обратите внимание, что значение может не содержать пробелов, иначе вы добавите больше элементов одновременно
  • 4
    Upvote для синтаксиса hashmap ["key"] = "value", который я тоже нашел отсутствующим в другом фантастически принятом ответе.
Показать ещё 1 комментарий
21

Вы можете дополнительно изменить интерфейс 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`
  • 1
    Большой! Вы можете даже повторить это: для i в $ (compgen -A переменная capitols); сделать hget "$ i" "" сделано
13
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
  • 4
    Всегда забавно видеть, что люди все еще не заботятся о произвольном внедрении кода или не понимают его.
  • 31
    Вздох, это кажется излишне оскорбительным и в любом случае неточным. Никто не помещал бы проверку ввода, экранирование или кодирование (см. Я действительно знаю) в кишки хэш-таблицы, а скорее в обертку и как можно скорее после ввода.
Показать ещё 3 комментария
10

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

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

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

hashtable=$(mktemp -d)

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

echo $value > $hashtable/$key

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

value=$(< $hashtable/$key)

Спектакль

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

  • 1
    Какая версия bash поддерживает mkdir -d ? (Не 4.3, в Ubuntu 14. Я бы прибегнул к mkdir /run/shm/foo , или, если он заполнил RAM, mkdir /tmp/foo .)
  • 1
    Возможно, вместо этого подразумевался mktemp -d ?
Показать ещё 4 комментария
9

Рассмотрим решение с использованием 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
  • 0
    Это так здорово. Спасибо
  • 1
    @CharlieMartin: чтение - очень мощная функция, которая используется многими программистами bash недостаточно. Это позволяет компактные формы обработки списков, похожих на lisp . Например, в приведенном выше примере мы можем убрать только первый элемент и сохранить оставшийся (т. Е. Концепцию, аналогичную first и rest в lisp), выполнив: IFS=$'|' read -r first rest <<< "$fields"
6

Я согласен с @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
5

Мне очень понравился 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()

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[@])
  • 0
    Я думаю, что это довольно аккуратный фрагмент. Это могло бы использовать небольшую очистку (не много, хотя). В моей версии я переименовал «ключ» в «пару» и сделал KEY и VALUE строчными (потому что я использую прописные буквы при экспорте переменных). Я также переименовал getHashKey в getHashValue и сделал ключ и значение локальным (хотя иногда вы бы хотели, чтобы они не были локальными). В getHashKeys я не присваиваю значение значению. Я использую точку с запятой для разделения, так как мои значения - это URL.
2

Две вещи, вы можете использовать память вместо /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

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

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

1

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

# Here 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 и друзьях.

0

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

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

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

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

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

unset statusCheck; declare -A statusCheck
0

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню