关于字典:如何在Bash中定义哈希表?

How to define hash tables in Bash?

什么是相似的Python字典,但在Bash中(应该适用于OS X和Linux)。


Bash 4

Bash 4原生支持此功能。确保您的脚本的hashbang是#!/usr/bin/env bash#!/bin/bash,因此您最终不会使用sh。确保您要么直接执行脚本,要么使用bash script执行script。 (实际上没有用Bash执行Bash脚本确实会发生,而且会让人感到困惑!)

通过执行以下操作声明关联数组:

1
declare -A animals

您可以使用常规数组赋值运算符填充元素。例如,如果您想要一张animal[sound(key)] = animal(value)的地图:

1
animals=( ["moo"]="cow" ["woof"]="dog")

或合并它们:

1
declare -A animals=( ["moo"]="cow" ["woof"]="dog")

然后像普通数组一样使用它们。使用animals['key']='value'设置值,"${animals[@]}"扩展值,使用"${!animals[@]}"(注意!)扩展键。别忘了引用它们:

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

Bash 3

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

首先:考虑升级到bash 4.这将使整个过程更加轻松。

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

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

首先,间接。

1
2
$ animals_moo=cow; sound=moo; i="animals_$sound"; echo"${!i}"
cow

其次,declare

1
2
$ sound=moo; animal=cow; declare"animals_$sound=$animal"; echo"$animals_moo"
cow

将他们聚集在一起:

1
2
3
4
5
6
7
8
9
# Set a value:
declare"array_$index=$value"

# Get a value:
arrayGet() {
    local array=$1 index=$2
    local i="${array}_$index"
    printf '%s'"${!i}"
}

我们来使用它:

1
2
3
4
5
$ 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,并完全避免此问题。


有参数替换,虽然它也可能是非PC的......就像间接一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/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.
"
"$KEY""$VALUE"
done

printf"%s is an extinct animal which likes to %s
"
"${ARRAY[1]%%:*}""${ARRAY[1]##*:}"

BASH 4的方式当然更好,但是如果你需要一个黑客......只有一个黑客会做。
您可以使用类似的技术搜索数组/哈希。


这就是我在这里寻找的:

1
2
3
4
5
6
7
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不起作用:

1
animals=( ["moo"]="cow" )


您可以进一步修改hput()/ hget()接口,以便命名哈希,如下所示:

1
2
3
4
5
6
7
hput() {
    eval"$1""$2"='$3'
}

hget() {
    eval echo '${'"$1$2"'#hash}'
}

然后

1
2
3
4
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组合也会快得多 - 根据我的经验,速度要快几倍。它也减少了记忆。

这是一种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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`


只需使用文件系统

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

哈希表创作

hashtable=$(mktemp -d)

添加元素

echo $value > $hashtable/$key

读一个元素

value=$(< $hashtable/$key)

性能

当然,它的速度慢,但不是那么慢。
我在我的机器上测试了它,带有SSD和btrfs,每秒大约有3000个元素读/写。


1
2
3
4
5
6
7
8
9
10
11
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`
1
2
$ sh hash.sh
Paris and Amsterdam and Madrid


考虑使用bash内置读取的解决方案,如下面的ufw防火墙脚本的代码片段所示。该方法具有使用尽可能多的定界字段集(不仅仅是2)的优点。我们用过|分隔符,因为端口范围说明符可能需要冒号,即6001:6010。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/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


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

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

    1
    animals=",moo:cow,woof:dog,"
  • 使用正则表达式提取值

    1
    2
    3
    get_animal {
        echo"$(expr"$animals" :".*,$1:\([^,]*\),.*")"
    }
  • 拆分字符串以列出项目

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    get_animal_items {
        arr=$(echo"${animals:1:${#animals}-2}" | tr",""
    "
    )
        for i in $arr
        do
            value="${i##*:}"
            key="${i%%:*}"
            echo"${value} likes to $key"
        done
    }
  • 现在你可以使用它:

    1
    2
    3
    4
    5
    $ animal = get_animal"moo"
    cow
    $ get_animal_items
    cow likes to moo
    dog likes to woof

    我真的很喜欢Al P的答案,但希望廉价执行的独特性,所以我更进一步 - 使用目录。有一些明显的限制(目录文件限制,文件名无效)但它应该适用于大多数情况。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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
    '
    $(hget ids"key$i") > /dev/null
    done

    hdestroy ids

    它在我的测试中也表现得更好一些。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ 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解决方案:

    在阅读一些答案时,我把一个快速的小函数放在一起,我想回馈一下,这可能对其他人有帮助。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    # 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[@])


    一位同事刚提到这个帖子。我在bash中独立实现了哈希表,并且它不依赖于版本4.我在2010年3月的一篇博客文章中(在此之前的一些答案......之前)在bash中标题为哈希表:

    我之前使用cksum进行哈希,但后来将Java的字符串hashCode翻译为本机bash / zsh。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 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和朋友。


    有两件事,你可以在任何内核2.6中使用内存代替/ tmp,使用/ dev / shm(Redhat)其他发行版可能会有所不同。另外hget可以使用如下读取重新实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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

    读/回声:

    1
    2
    3
    4
    5
    6
    $ time echo $(hget FD oracle)
    3

    real    0m0.004s
    user    0m0.000s
    sys     0m0.004s

    在多次调用中,我从未见过少于50%的改进。
    由于使用/dev/shm,这可以全部归因于fork over head。


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

    至于bash 3中不太好的方法,这里有一个参考而不是可能有帮助:http://mywiki.wooledge.org/BashFAQ/006


    我也使用了bash4方式,但我找到了烦人的bug。

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

    1
    2
    3
    4
    5
    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没有问题,其中附加现有密钥意味着如果已经存在,则替代该实际值。

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

    1
    unset statusCheck; declare -A statusCheck

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

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