Каков эквивалент словарей Python, но в Bash (должен работать через OS X и Linux).
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
До 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
повозки, запряженных волов.
declare -A
, я не могу поверить, что никогда не использовал это раньше! Я запрограммировал Bash на 10 лет.
Здесь подстановка параметров, хотя это может быть и не-ПК... например, косвенное.
#!/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 лучше, конечно, но если вам нужен взлом... только взломать будет. Вы можете искать массив/хэш с аналогичными методами.
VALUE=${animal#*:}
чтобы защитить случай, когда ARRAY[$x]="caesar:come:see:conquer"
Вот что я искал здесь:
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 "$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`
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
Файловая система представляет собой древовидную структуру, которую можно использовать в качестве хэш-карты. Ваша хеш-таблица будет временным каталогом, ваши ключи будут именами файлов, а ваши значения будут содержимым файла. Преимущество заключается в том, что он может обрабатывать огромные хеш-карты и не требует специальной оболочки.
hashtable=$(mktemp -d)
echo $value > $hashtable/$key
value=$(< $hashtable/$key)
Конечно, его медленно, но не то, что медленно. Я протестировал его на своей машине, с SSD и btrfs, и он делает около 3000 элементов чтения/записи в секунду.
mkdir -d
? (Не 4.3, в Ubuntu 14. Я бы прибегнул к mkdir /run/shm/foo
, или, если он заполнил RAM, mkdir /tmp/foo
.)
mktemp -d
?
Рассмотрим решение с использованием 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
IFS=$'|' read -r first rest <<< "$fields"
Я согласен с @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
Мне очень понравился 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()
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[@])
Две вещи, вы можете использовать память вместо /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
.
До bash 4 нет подходящего способа использования ассоциативных массивов в bash. Лучше всего использовать интерпретируемый язык, который фактически поддерживает такие вещи, как awk. С другой стороны, bash 4 поддерживает их.
Что касается менее хороших способов в bash 3, вот ссылка, которая может помочь: http://mywiki.wooledge.org/BashFAQ/006
Сотрудник только что упомянул эту тему. Я самостоятельно внедрил хэш-таблицы в 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 и друзьях.
Я создаю HashMaps в bash 3 с использованием динамических переменных. Я объяснил, как это работает в моем ответе на: Ассоциативные массивы в сценариях оболочки
Также вы можете взглянуть на shell_map, которая представляет собой реализацию HashMap, выполненную в bash 3.
Я также использовал метод 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
Чтобы получить немного больше производительности, помните, что grep имеет функцию остановки, чтобы остановиться, когда найдет n-й матч, в этом случае n будет равно 1.
grep --max_count = 1... или grep -m 1...