如何在Bash中定义哈希表?

什么是等效的python字典,但在Bash中(应该在OS X和Linux上工作)。

639667 次浏览
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

您可以进一步修改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`

这允许您定义其他不冲突的地图(例如,按首都进行国家查找的“首都”)。但是,无论哪种方式,我认为您都会发现这在性能方面都非常糟糕。

编辑:上面的修改版本,支持非字母数字字符的键

hashKey() {
# replace non-alphanumeric characters with underscore to make keys valid BASH identifiers
echo "$1_$2" | sed -E "s/[^a-zA-Z0-9]+/_/g" | sed -E "s/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+\$//g"
}


hashPut() {
local KEY=`hashKey $1 $2`
eval "$KEY"="$3"
}


hashGet() {
local KEY=`hashKey $1 $2`
echo "${!KEY}"
}

结束编辑

如果你真的想要快速的哈希查找,有一个可怕的,可怕的黑客实际上工作得非常好。它是这样的:将你的键/值写入一个临时文件,每行一个,然后使用grep"^$key"'将它们取出来,使用带有Cut或awk或ses或任何东西的管道来检索值。

就像我说的,这听起来很糟糕,听起来应该很慢,做各种不必要的IO,但实际上它非常快(磁盘缓存很棒,不是吗?),即使对于非常大的哈希表。你必须自己强制密钥唯一性,等等。即使你只有几百个条目,输出文件/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`

在bash 4之前,没有很好的方法在bash中使用关联数组。你最好的选择是使用实际支持此类东西的解释语言,例如awk。另一方面,bash 4确实支持它们。

至于bash 3中的更少好方法,这里有一个参考可能会有所帮助:http://mywiki.wooledge.org/BashFAQ/006

bash4

Bash 4本身支持此功能。确保您的脚本的哈希邦是#!/usr/bin/env bash#!/bin/bash,这样您就不会最终使用sh。确保您要么直接执行脚本,要么使用bash script执行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

bash3

在bash 4之前,你没有关联数组。不要使用eval来模仿它们。避免eval像瘟疫一样,因为它是shell脚本的瘟疫。最重要的原因是eval将您的数据视为执行代码(还有许多其他原因)。

首先也是最重要的:考虑升级到bash 4。这将使整个过程对您来说更容易。

如果有原因无法升级,declare是一个更安全的选择。它不像eval那样评估bash代码的数据,因此不允许任意代码注入。

让我们通过介绍概念来准备答案:

第一,间接。

$ 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不能放在函数中。在bash函数中使用declare会将其创建的变量当地转换为该函数的范围,这意味着我们不能使用它访问或修改全局数组。(在bash 4中,您可以使用declare -g声明全局变量-但在bash 4中,您可以首先使用关联数组,避免这种解决方法。)

总结:

  • 升级到bash 4并对关联数组使用declare -A
  • 如果无法升级,请使用declare选项。
  • 考虑使用awk代替并完全避免这个问题。

有两件事,你可以在任何内核2.6中使用内存而不是 /tmp通过使用 /dev/shm(Redhat)其他发行版可能会有所不同。

function hget {


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

此外,通过假设所有键都是唯一的,返回短路读取循环并防止必须读取所有条目。如果您的实现可以有重复的键,那么只需省略返回。这节省了读取和分叉grep和awk的费用。对这两种实现使用 /dev/shm产生了以下使用time 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的分叉。

我真的很喜欢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

我只是想来帮忙干杯!

编辑:添加h的()

有参数替换,尽管它也可能是非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" )

一个同事刚刚提到了这个线程。我在bash中独立实现了哈希表,它不依赖于版本4。来自我2010年3月的一篇名为bash中的哈希表的博客文章(在这里的一些答案之前…):

以前使用cksum进行哈希,但后来将Java的string hashCode翻译为原生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和朋友中除外。

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[@])

我同意@lhunath和其他人的观点,即关联数组是Bash 4的方式。如果你坚持使用Bash 3(OSX,无法更新的旧发行版),你也可以使用exr,它应该无处不在,字符串和正则表达式。我喜欢它,特别是当字典不是太大时。

  1. 选择2个不在键和值中使用的分隔符(例如','和':')
  2. 将地图写成字符串(注意分隔符','也在开头和结尾)

    animals=",moo:cow,woof:dog,"
    
  3. Use a regex to extract the values

    get_animal {
    echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
    
  4. Split the string to list the items

    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
    }
    

Now you can use it:

$ animal = get_animal "moo"
cow
$ get_animal_items
cow likes to moo
dog likes to woof

我也用bash4的方式,但我发现和烦人的bug。

我需要动态更新关联数组内容,所以我使用了这种方式:

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中,如果已经存在,就会追加该值。例如,在一些重复之后,值的内容是“check KOcheck KOallCheckOK”,这并不好。

bash 4.3.39没有问题,其中附加一个存在的键意味着如果已经存在,则替换实际值。

我解决了这个问题,只是在圆柱之前清理/声明statusCheck关联数组:

unset statusCheck; declare -A statusCheck

考虑一个使用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

我在bash 3中使用动态变量创建HashMap。我在对Shell脚本中的关联数组的回答中解释了它是如何工作的

您还可以查看shell_map,它是bash 3中的HashMap实现。

只需使用文件系统

文件系统是一个树结构,可以用作哈希映射。 您的哈希表将是一个临时目录,您的键将是文件名,您的值将是文件内容。优点是它可以处理巨大的哈希图,并且不需要特定的shell。

哈希表创建

hashtable=$(mktemp -d)

添加一个元素

echo $value > "$hashtable/$key"

读取一个元素

value=$(< "$hashtable/$key")

性能

当然,它很慢,但不是慢。 我在我的机器上测试了它,使用SSD和btrfs,它大约在每秒3000个元素读/写左右。