如何在Bash中定义哈希表?

Rei*_*ica 520 bash dictionary associative-array hashtable

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

lhu*_*ath 865

Bash 4

Bash 4原生支持此功能.确保你的脚本hashbang是#!/usr/bin/env bash#!/bin/bash或其他任何引用sh,而不是script.确保你正在执行你的脚本,而不是做一些愚蠢的事情bash script会导致你的animal[sound(key)] = animal(value)hashbang被忽略.这是基本的东西,但是很多人都在努力,因此重新迭代.

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

declare -A animals
Run Code Online (Sandbox Code Playgroud)

您可以使用常规数组赋值运算符填充元素:

animals=( ["moo"]="cow" ["woof"]="dog")
Run Code Online (Sandbox Code Playgroud)

或合并它们:

declare -A animals=( ["moo"]="cow" ["woof"]="dog")
Run Code Online (Sandbox Code Playgroud)

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

echo "${animals[moo]}"
for sound in "${!animals[@]}"; do echo "$sound - ${animals[$sound]}"; done
Run Code Online (Sandbox Code Playgroud)

Bash 3

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

首先:只考虑升级到bash 4.认真. 现在的未来,停止生活在过去,并通过强迫你的代码上的愚蠢破碎和丑陋的黑客而遭受它痛苦,每个可怜的灵魂都坚持维持它.

如果你有一些愚蠢的借口,为什么你" 无法升级 ",eval是一个更安全的选择.它没有像bash代码一样评估数据eval,因此它不允许任意代码注入.

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

第一,间接(严肃;从不使用这个,除非你患有精神病或者有其他不好的借口来写黑客).

$ animals_moo=cow; sound=moo; i="animals_$sound"; echo "${!i}"
cow
Run Code Online (Sandbox Code Playgroud)

其次,declare:

$ sound=moo; animal=cow; declare "animals_$sound=$animal"; echo "$animals_moo"
cow
Run Code Online (Sandbox Code Playgroud)

将他们聚集在一起:

# Set a value:
declare "array_$index=$value"

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

我们来使用它:

$ sound=moo
$ animal=cow
$ declare "animals_$sound=$animal"
$ arrayGet animals "$sound"
cow
Run Code Online (Sandbox Code Playgroud)

注意:eval不能放入功能.declare在bash函数内部的任何使用都会将它在本地创建的变量转换为该函数的范围,这意味着我们无法使用它来访问或修改全局数组.(在bash 4中你可以使用declare -g来声明全局变量 - 但是在bash 4中,你应该首先使用关联数组,而不是这个hack.)

摘要

升级到bash 4并使用declare.如果你不能,可以考虑完全切换到declare如上所述的丑陋黑客之前.绝对要远离declare -Ahackery.

  • @ken这是一个许可问题.OSX上的Bash卡在最新的非GPLv3许可版本中. (13认同)
  • 无法升级:我在Bash中编写脚本的唯一原因是"随处运行"可移植性.所以依靠Bash的非通用特性来规定这种方法.这是一种耻辱,因为否则它对我来说将是一个很好的解决方案! (4认同)
  • 令人遗憾的是OSX默认为Bash 3,因为这代表了许多人的"默认".我认为ShellShock恐慌可能是他们需要的推动但显然不是. (3认同)
  • @jww Apple 不会将 GNU bash 升级到 3 以上,因为它对 GPLv3 怀有恶意。但这不应该是一种威慑。`brew install bash` http://brew.sh/ (3认同)
  • ...或者[`sudo port install bash`](https://macports.org/),对于那些(明智地,恕我直言)不愿意在没有明确的每进程权限升级的情况下为所有可写用户创建PATH中的目录. (2认同)

Bub*_*off 116

有参数替换,虽然它也可能是非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]##*:}"
Run Code Online (Sandbox Code Playgroud)

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

  • 我会改为`VALUE = $ {animal#*:}`来保护`ARRAY [$ x] ="caesar:come:see:conquer"` (5认同)
  • 如果在键或值中有空格,在$ {ARRAY [@]}周围放置双引号也很有用,如'for animal in'$ {ARRAY [@]}"; do` (2认同)
  • @CoDEmanX:这是一个_hack_,一个聪明而优雅但仍然初级的_workaround_,以帮助那些仍然停留在 2007 年 Bash 3.x 中的可怜人。您不能期望在如此简单的代码中实现“正确的哈希图”或效率考虑。 (2认同)

akt*_*ivb 73

这就是我在这里寻找的:

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
Run Code Online (Sandbox Code Playgroud)

这对bash 4.1.5不起作用:

animals=( ["moo"]="cow" )
Run Code Online (Sandbox Code Playgroud)

  • upvote for hashmap ["key"] ="value"语法,我也发现这个语法在其他方面很难接受. (6认同)
  • 请注意,该值不能包含空格,否则您一次要添加更多元素 (2认同)

Al *_* P. 24

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

hput() {
    eval "$1""$2"='$3'
}

hget() {
    eval echo '${'"$1$2"'#hash}'
}
Run Code Online (Sandbox Code Playgroud)

然后

hput capitals France Paris
hput capitals Netherlands Amsterdam
hput capitals Spain Madrid
echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain`
Run Code Online (Sandbox Code Playgroud)

这使您可以定义其他不冲突的地图(例如,"资本城市"进行国家查找的"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`
Run Code Online (Sandbox Code Playgroud)

  • 伟大的!你甚至可以迭代它: for i in $(compgen -A variable capitols); 做 hget "$i" "" 完成 (2认同)

lov*_*soa 16

只需使用文件系统

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

哈希表创作

hashtable=$(mktemp -d)

添加元素

echo $value > $hashtable/$key

读一个元素

value=$(< $hashtable/$key)

性能

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

  • 也许是“mktemp -d”? (3认同)
  • 好奇`$ value = $(<$ hashtable/$ key)`和`value = $(<$ hashtable/$ key)`有什么区别?谢谢! (2认同)
  • “在我的机器上测试过”这听起来像是在 SSD 上烧个洞的好方法。并非所有 Linux 发行版默认都使用 tmpfs。 (2认同)

Dig*_*oss 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`
Run Code Online (Sandbox Code Playgroud)
$ sh hash.sh
Paris and Amsterdam and Madrid
Run Code Online (Sandbox Code Playgroud)

  • 叹了口气,这似乎是不必要的侮辱,无论如何都是不准确的.人们不会在哈希表的内部放置输入验证,转义或编码(参见,我实际上知道),而是在输入后尽快放入包装器中. (31认同)

Asy*_*abs 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
Run Code Online (Sandbox Code Playgroud)

  • @CharlieMartin:读取是一项非常强大的功能,许多bash程序员并未充分利用它。它允许_lisp-like_列表处理的紧凑形式。例如,在上面的示例中,我们可以通过执行以下操作来去除第一个元素并保留其余元素(即与lisp中的_first_和_rest_类似的概念):'IFS = $'|' 读取-r第一休息&lt;&lt;&lt;“ $ fields”` (2认同)

mar*_*rco 6

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

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

    animals=",moo:cow,woof:dog,"
    
    Run Code Online (Sandbox Code Playgroud)
  3. 使用正则表达式提取值

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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
    }
    
    Run Code Online (Sandbox Code Playgroud)

现在你可以使用它:

$ animal = get_animal "moo"
cow
$ get_animal_items
cow likes to moo
dog likes to woof
Run Code Online (Sandbox Code Playgroud)


Col*_*eld 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
Run Code Online (Sandbox Code Playgroud)

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

$ 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
Run Code Online (Sandbox Code Playgroud)

只是想我会投入.干杯!

编辑:添加hdestroy()


Ada*_*atz 5

一位同事刚刚提到了这个话题。我已经在 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)
Run Code Online (Sandbox Code Playgroud)

它不是双向的,内置的方式要好得多,但无论如何都不应该真正使用。Bash 是为了快速一次性的,这样的事情应该很少涉及可能需要哈希的复杂性,除了你~/.bashrc和朋友之外。