什么是 Python dictionaries 但在 Bash 中的等价物(应该在 OS X 和 Linux 上工作)。
重击 4
Bash 4 本机支持此功能。确保您的脚本的 hashbang 是 #!/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")
然后像普通数组一样使用它们。利用
动物['key']='value' 设置值
"${animals[@]}" 扩展值
"${!animals[@]}" (注意!)展开键
不要忘记引用它们:
echo "${animals[moo]}"
for sound in "${!animals[@]}"; do echo "$sound - ${animals[$sound]}"; done
重击 3
在 bash 4 之前,您没有关联数组。 不要使用 eval
来模仿它们。避免像瘟疫一样的 eval
,因为它是 shell 脚本的瘟疫。最重要的原因是 eval
将您的数据视为可执行代码(还有许多其他原因)。
首先也是最重要的:考虑升级到 bash 4。这将使整个过程对您来说更容易。
如果有无法升级的原因,declare
是一个更安全的选择。它不像 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
不能放在函数中。在 bash 函数中对 declare
的任何使用都会将它创建的变量 local 转换为该函数的范围,这意味着我们无法使用它访问或修改全局数组。 (在 bash 4 中,您可以使用 declare -g
声明全局变量 - 但在 bash 4 中,您可以首先使用关联数组,避免这种解决方法。)
概括:
升级到 bash 4 并将 declare -A 用于关联数组。
如果无法升级,请使用声明选项。
考虑改用 awk 并完全避免该问题。
有参数替换,尽管它也可能是非 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 方式当然更好,但如果你需要一个 hack ......只有一个 hack 会做。您可以使用类似的技术搜索数组/哈希。
VALUE=${animal#*:}
以保护 ARRAY[$x]="caesar:come:see:conquer"
for animal in "${ARRAY[@]}"; do
这就是我在这里寻找的:
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" )
只需使用文件系统
文件系统是一种树结构,可以用作哈希映射。您的哈希表将是一个临时目录,您的键将是文件名,您的值将是文件内容。优点是它可以处理巨大的哈希图,并且不需要特定的外壳。
哈希表创建
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
是用来代替的?
$value=$(< $hashtable/$key)
和 value=$(< $hashtable/$key)
有什么区别?谢谢!
您可以进一步修改 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”)。但是,无论哪种方式,我认为您会发现这一切都非常糟糕,就性能而言。
如果你真的想要快速的哈希查找,那么有一个可怕的、可怕的 hack 实际上工作得很好。就是这样:将您的键/值写入一个临时文件,每行一个,然后使用 '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`
考虑使用 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
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
${var#start}
从存储在变量 var 中的值的开头删除文本 start。
我同意@lhunath 和其他人的观点,即关联数组是 Bash 4 的方式。如果你坚持使用 Bash 3(OSX,你无法更新的旧发行版),你也可以使用 expr,它应该无处不在,一个字符串和正则表达式。我喜欢它,尤其是当字典不太大的时候。
选择 2 个不会在键和值中使用的分隔符(例如 ',' 和 ':' ) 将地图写成字符串(注意分隔符 ',' 也在开头和结尾处)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} 喜欢 $key" 完成 }
现在你可以使用它了:
$ 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 中独立实现了哈希表,它不依赖于版本 4。来自我在 2010 年 3 月的一篇博客文章(在此处的一些答案之前......),标题为 Hash tables in bash:
我 previously 使用 cksum
进行散列,但后来将 Java's 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
和朋友中。
有两件事,您可以在任何内核 2.6 中使用内存而不是 /tmp,方法是使用 /dev/shm (Redhat),其他发行版可能会有所不同。 hget 也可以使用 read 重新实现,如下所示:
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 确实支持它们。
至于 less bash 3 中的好方法,这里有一个参考,可能会有所帮助:http://mywiki.wooledge.org/BashFAQ/006
重击 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[@])
我也使用了 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 没有问题,其中附加现有密钥意味着如果已经存在,则替换实际值。
我解决了这个问题,只是在 cicle 之前清理/声明 statusCheck 关联数组:
unset statusCheck; declare -A statusCheck
我使用动态变量在 bash 3 中创建 HashMaps。我在回答以下问题时解释了它的工作原理:Associative arrays in Shell scripts
您还可以查看 shell_map,它是 bash 3 中的 HashMap 实现。
brew install bash
brew.shsudo port install bash
,对于那些(明智地,恕我直言)不愿意在 PATH 中为所有用户创建可写目录而无需显式每个进程权限提升的人。