如何在Bash中定义哈希表?


Answers:


938

重击4

Bash 4本机支持此功能。确保您脚本的hashbang为,#!/usr/bin/env bash否则#!/bin/bash您最终不会使用sh。确保您是直接执行脚本,还是script使用bash script。(实际上不执行与巴什一个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像瘟疫一样,因为它 shell脚本编写的祸害。最重要的原因是eval将数据视为可执行代码(还有许多其他原因)。

首先,请考虑升级到bash4。这将使整个过程更加轻松。

如果由于某种原因您无法升级,那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不能放在函数中。declarebash函数内部的任何使用都会使它创建的变量局部于该函数的作用域,这意味着我们无法使用它访问或修改全局数组。(在bash 4中,可以使用declare -g声明全局变量-但是在bash 4中,可以首先使用关联数组,从而避免了这种变通方法。)

摘要:

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

1
@Richard:大概您实际上并没有使用bash。您的hashbang是sh而不是bash,还是用sh调用代码?尝试在声明之前将其正确放置:echo“ $ BASH_VERSION $ POSIXLY_CORRECT”,它应该输出4.x而不是y
lhunath 2012年

5
无法升级:我在Bash中编写脚本的唯一原因是为了“在任何地方运行”的可移植性。因此,依靠Bash的非通用功能可以解决此问题。真可惜,因为否则对我来说这将是一个很好的解决方案!
Steve Pitchers 2014年

3
令人遗憾的是,OSX仍默认为Bash 3,因为这代表了很多人的“默认”。我以为ShellShock恐慌可能是他们需要的推动力,但显然不是。
2014年

13
@ken这是许可问题。OSX上的Bash停留在最新的非GPLv3许可版本上。
2014年

2
...或sudo port install bash,对于那些不愿在所有用户的PATH中创建可写目录的用户(明智的IMHO),而无需显式逐进程特权升级。
查尔斯·达菲

125

有参数替换,尽管它也可以是非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方式当然更好,但是如果您需要黑客手段,则只有黑客能做到。您可以使用类似的技术搜索数组/哈希。


5
VALUE=${animal#*:}为了保护这种情况,我将进行更改ARRAY[$x]="caesar:come:see:conquer"
glenn jackman 2011年

2
万一键或值中有空格,在$ {ARRAY [@]}周围加上双引号也很有用,例如for animal in "${ARRAY[@]}"; do
devguydavid 2012年

1
但是效率不是很差吗?我在想O(n * m)是否要与另一个键列表进行比较,而不是使用具有适当哈希图的O(n)(恒定时间查找,单个键为O(1))。
CodeManX

1
对于具有Perl,Python甚至bash 4背景的用户,想法不是效率,而是知识/可读性。允许您以类似的方式编写。
布本诺夫2015年

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
支持hashmap [“ key”] =“ value”语法,我也发现该语法从原本奇妙的可接受答案中消失了。
thomanski '16

@ rubo77 key都没有,它添加了多个密钥。任何解决方法?
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”'将其取出,使用带有cut或awk或sed的管道或其他检索值的管道。

就像我说的那样,这听起来很糟糕,听起来应该很慢,并且执行各种不必要的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`

1
大!您甚至可以迭代它:对于$(compgen -A可变国会大厦)中的i;做hget“ $ i”“”完成
zhaorufei

22

只需使用文件系统

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

哈希表创建

hashtable=$(mktemp -d)

添加元素

echo $value > $hashtable/$key

读取元素

value=$(< $hashtable/$key)

性能

当然,它的速度慢,但不能慢。我在装有SSD和btrfs的计算机上对其进行了测试,每秒可读取3000个元素


1
哪个版本的bash支持mkdir -d?(在Ubuntu 14上不是4.3,我会求助于mkdir /run/shm/foo,或者如果RAM装满了,mkdir /tmp/foo。)
Camille Goudeseune17年

1
也许mktemp -d是相反的意思?
Reid Ellis

2
好奇$value=$(< $hashtable/$key)和之间有什么区别value=$(< $hashtable/$key)?谢谢!
Helin Wang

1
“在我的机器上测试过”这听起来像是一种在SSD上烧洞的好方法。并非所有Linux发行版默认都使用tmpfs。
kirbyfan64sos

我正在处理约50000个哈希。Perl和PHP在不到1/2秒的时间内完成了工作。节点在1秒左右。FS选项听起来很慢。但是,是否可以以某种方式确保文件仅存在于RAM中?
罗尔夫(Rolf)'18

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可以解释一下eval echo'$ {hash'“ $ 1”'#hash}'中#hash的用途。对我来说,似乎我只是在评论而已。#hash在这里有什么特殊含义吗?
桑杰

@Sanjay ${var#start}删除文本开始从存储在变量值的开头变种
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:read是一个非常强大的功能,许多bash程序员没有充分利用它。它允许紧凑形式的类似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

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


3

两件事,您可以通过使用/ dev / shm(Redhat)在任何内核2.6中使用内存代替/ tmp,其他发行版可能有所不同。同样,可以使用以下方式重新实现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,在搜索最后一个条目的3个条目哈希上使用时间hget产生了以下内容:

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年3月我的一篇博客文章(在此给出一些答案之前...),标题为bash中的哈希表

以前曾经cksum进行过哈希处理,但此后将Java的字符串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和您的朋友。


答案中的链接很可怕!如果单击它,则会陷入重定向循环。请更新。
拉基布'19

1
@MohammadRakibAmin –是的,我的网站已关闭,我怀疑我会复活我的博客。我已将以上链接更新为存档版本。感谢您的关注!
亚当·卡兹

2

在bash 4之前,没有在bash中使用关联数组的好方法。最好的选择是使用实际上支持诸如awk之类的解释语言。另一方面,bash 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追加到dict中的现有键会导致附加值(如果已存在)。因此,例如,在重复几次之后,该值的内容为“ checkKOcheckKOallCheckOK”,这不好。

bash 4.3.39没问题,在其中添加一个现有密钥意味着可以替代实际值(如果已经存在)。

我解决了这一点,只是在提示之前清洗/声明了statusCheck关联数组:

unset statusCheck; declare -A statusCheck

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.