Shell 脚本中的关联数组

我们需要一个模拟关联数组或类似映射的数据结构的脚本来编写 shell 脚本。有人能告诉我们是怎么做到的吗?

237236 次浏览

现在回答这个问题。

下面的脚本模拟了 shell 脚本中的关联数组。

Map 只是一个永远不会结束的字符串,将 keyValuePair 保存为 —— name = Irfan —— sign = SSE —— Company = My: SP: Own: SP: Company

对于值,空格被替换为’: SP:’

put() {
if [ "$#" != 3 ]; then exit 1; fi
mapName=$1; key=$2; value=`echo $3 | sed -e "s/ /:SP:/g"`
eval map="\"\$$mapName\""
map="`echo "$map" | sed -e "s/--$key=[^ ]*//g"` --$key=$value"
eval $mapName="\"$map\""
}


get() {
mapName=$1; key=$2; valueFound="false"


eval map=\$$mapName


for keyValuePair in ${map};
do
case "$keyValuePair" in
--$key=*) value=`echo "$keyValuePair" | sed -e 's/^[^=]*=//'`
valueFound="true"
esac
if [ "$valueFound" == "true" ]; then break; fi
done
value=`echo $value | sed -e "s/:SP:/ /g"`
}


put "newMap" "name" "Irfan Zulfiqar"
put "newMap" "designation" "SSE"
put "newMap" "company" "My Own Company"


get "newMap" "company"
echo $value


get "newMap" "name"
echo $value

编辑: 刚刚添加了另一个方法来获取所有的键。

getKeySet() {
if [ "$#" != 1 ];
then
exit 1;
fi


mapName=$1;


eval map="\"\$$mapName\""


keySet=`
echo $map |
sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g"
`
}

为了添加到 Irfan 的回答中,这里有一个更短更快的 get()版本,因为它不需要对映射内容进行迭代:

get() {
mapName=$1; key=$2


map=${!mapName}
value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}

我觉得你应该退一步想想地图或者关联数组到底是什么。它只是一种存储给定键值的方法,并快速有效地获取该值。您可能还希望能够迭代这些键以检索每个键值对,或者删除键及其相关的值。

现在,考虑一个在 shell 脚本中一直使用的数据结构,甚至在没有编写脚本的 shell 中也是如此,它具有这些属性。被难住了?是文件系统。

实际上,在 shell 编程中,只需要一个临时目录就可以了解关联数组。Abc0是你的关联数组构造函数:

prefix=$(basename -- "$0")
map=$(mktemp -dt ${prefix})
echo >${map}/key somevalue
value=$(cat ${map}/key)

如果你不喜欢使用 echocat,你可以写一些小的包装器; 这些包装器是模仿 Irfan 的,虽然它们只是输出值,而不是设置任意的变量,比如 $value:

#!/bin/sh


prefix=$(basename -- "$0")
mapdir=$(mktemp -dt ${prefix})
trap 'rm -r ${mapdir}' EXIT


put() {
[ "$#" != 3 ] && exit 1
mapname=$1; key=$2; value=$3
[ -d "${mapdir}/${mapname}" ] || mkdir "${mapdir}/${mapname}"
echo $value >"${mapdir}/${mapname}/${key}"
}


get() {
[ "$#" != 2 ] && exit 1
mapname=$1; key=$2
cat "${mapdir}/${mapname}/${key}"
}


put "newMap" "name" "Irfan Zulfiqar"
put "newMap" "designation" "SSE"
put "newMap" "company" "My Own Company"


value=$(get "newMap" "company")
echo $value


value=$(get "newMap" "name")
echo $value

编辑 : 这种方法实际上比提问者建议的使用 sed 的线性搜索快得多,而且更加健壮(它允许键和值包含-,= ,space,qnd“ : SP:”)。它使用文件系统的事实并不会使它变慢; 实际上,除非您调用 sync,否则这些文件永远不会被写入磁盘; 对于像这样生命周期短的临时文件,它们中的许多文件永远不会被写入磁盘并不是不可能的。

我使用以下驱动程序对 Irfan 的代码、 Jerry 对 Irfan 代码的修改以及我的代码做了一些基准测试:

#!/bin/sh


mapimpl=$1
numkeys=$2
numvals=$3


. ./${mapimpl}.sh    #/ <- fix broken stack overflow syntax highlighting


for (( i = 0 ; $i < $numkeys ; i += 1 ))
do
for (( j = 0 ; $j < $numvals ; j += 1 ))
do
put "newMap" "key$i" "value$j"
get "newMap" "key$i"
done
done

结果:

$ time ./driver.sh irfan 10 5


real    0m0.975s
user    0m0.280s
sys     0m0.691s


$ time ./driver.sh brian 10 5


real    0m0.226s
user    0m0.057s
sys     0m0.123s


$ time ./driver.sh jerry 10 5


real    0m0.706s
user    0m0.228s
sys     0m0.530s


$ time ./driver.sh irfan 100 5


real    0m10.633s
user    0m4.366s
sys     0m7.127s


$ time ./driver.sh brian 100 5


real    0m1.682s
user    0m0.546s
sys     0m1.082s


$ time ./driver.sh jerry 100 5


real    0m9.315s
user    0m4.565s
sys     0m5.446s


$ time ./driver.sh irfan 10 500


real    1m46.197s
user    0m44.869s
sys     1m12.282s


$ time ./driver.sh brian 10 500


real    0m16.003s
user    0m5.135s
sys     0m10.396s


$ time ./driver.sh jerry 10 500


real    1m24.414s
user    0m39.696s
sys     0m54.834s


$ time ./driver.sh irfan 1000 5


real    4m25.145s
user    3m17.286s
sys     1m21.490s


$ time ./driver.sh brian 1000 5


real    0m19.442s
user    0m5.287s
sys     0m10.751s


$ time ./driver.sh jerry 1000 5


real    5m29.136s
user    4m48.926s
sys     0m59.336s


如果您主要关心的不是可移植性,那么另一种选择是使用内置在 shell 中的关联数组。这应该可以在 bash 4.0、 ksh 和 zsh 中使用(现在可以在大多数主流发行版上使用,但除非您自己安装,否则不能在 OS X 上使用) :

declare -A newmap
newmap[name]="Irfan Zulfiqar"
newmap[designation]=SSE
newmap[company]="My Own Company"


echo ${newmap[company]}
echo ${newmap[name]}

根据 shell 的不同,您可能需要执行 typeset -A newmap而不是 declare -A newmap,或者在某些情况下可能根本不需要。

正如前面提到的,我发现最好的方法是写出文件的 key/val,然后使用 grep/awk 检索它们。这听起来像是各种各样不必要的 IO,但是磁盘缓存发挥了作用,使其非常高效——比使用上述方法(如基准测试所示)在内存中存储它们要快得多。

这里有一个我喜欢的快速、简洁的方法:

hinit() {
rm -f /tmp/hashmap.$1
}


hput() {
echo "$2 $3" >> /tmp/hashmap.$1
}


hget() {
grep "^$2 " /tmp/hashmap.$1 | awk '{ print $2 };'
}


hinit capitols
hput capitols France Paris
hput capitols Netherlands Amsterdam
hput capitols Spain Madrid


echo `hget capitols France` and `hget capitols Netherlands` and `hget capitols Spain`

如果希望对每个键强制执行单值,还可以在 hput ()中执行一些 grep/sed 操作。

Bash4本身就支持这一点。不要使用 grepeval,它们是最丑陋的黑客。

有关详细的答案和示例代码,请参阅: Https://stackoverflow.com/questions/3467959

又是一个非暴力四人组。

#!/bin/bash


# A pretend Python dictionary with bash 3
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


echo -e "${ARRAY[1]%%:*} is an extinct animal which likes to ${ARRAY[1]#*:}\n"

您还可以抛出一个 if 语句用于在其中进行搜索。 或者别的什么。

多么遗憾,我以前没有看到这个问题-我已经写了库 外壳框架,其中包含其他地图(关联数组)。它的最后一个版本可以找到 给你

例如:

#!/bin/bash
#include map library
shF_PATH_TO_LIB="/usr/lib/shell-framework"
source "${shF_PATH_TO_LIB}/map"


#simple example get/put
putMapValue "mapName" "mapKey1" "map Value 2"
echo "mapName[mapKey1]: $(getMapValue "mapName" "mapKey1")"


#redefine old value to new
putMapValue "mapName" "mapKey1" "map Value 1"
echo "after change mapName[mapKey1]: $(getMapValue "mapName" "mapKey1")"


#add two new pairs key/values and print all keys
putMapValue "mapName" "mapKey2" "map Value 2"
putMapValue "mapName" "mapKey3" "map Value 3"
echo -e "mapName keys are \n$(getMapKeys "mapName")"


#create new map
putMapValue "subMapName" "subMapKey1" "sub map Value 1"
putMapValue "subMapName" "subMapKey2" "sub map Value 2"


#and put it in mapName under key "mapKey4"
putMapValue "mapName" "mapKey4" "subMapName"


#check if under two key were placed maps
echo "is map mapName[mapKey3]? - $(if isMap "$(getMapValue "mapName" "mapKey3")" ; then echo Yes; else echo No; fi)"
echo "is map mapName[mapKey4]? - $(if isMap "$(getMapValue "mapName" "mapKey4")" ; then echo Yes; else echo No; fi)"


#print map with sub maps
printf "%s\n" "$(mapToString "mapName")"
####################################################################
# Bash v3 does not support associative arrays
# and we cannot use ksh since all generic scripts are on bash
# Usage: map_put map_name key value
#
function map_put
{
alias "${1}$2"="$3"
}


# map_get map_name key
# @return value
#
function map_get
{
alias "${1}$2" | awk -F"'" '{ print $2; }'
}


# map_keys map_name
# @return map keys
#
function map_keys
{
alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }'
}

例如:

mapName=$(basename $0)_map_
map_put $mapName "name" "Irfan Zulfiqar"
map_put $mapName "designation" "SSE"


for key in $(map_keys $mapName)
do
echo "$key = $(map_get $mapName $key)
done

我用以下方法修改了瓦迪姆的溶液:

####################################################################
# Bash v3 does not support associative arrays
# and we cannot use ksh since all generic scripts are on bash
# Usage: map_put map_name key value
#
function map_put
{
alias "${1}$2"="$3"
}


# map_get map_name key
# @return value
#
function map_get {
if type -p "${1}$2"
then
alias "${1}$2" | awk -F "'" '{ print $2; }';
fi
}


# map_keys map_name
# @return map keys
#
function map_keys
{
alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }'
}

更改是 map _ get,以防止在请求一个不存在的键时返回错误,尽管副作用是它也会默认忽略丢失的映射,但它更适合我的用例,因为我只是想检查一个键,以便在循环中跳过项。

几年前,我为 bash 编写了脚本库,它支持关联数组以及其他特性(日志记录、配置文件、对命令行参数的扩展支持、生成帮助、单元测试等)。该库包含一个用于关联数组的包装器,并自动切换到适当的模型(内部用于 bash4,模拟用于以前的版本)。它被称为 shell 框架,托管在 Origo.ethz.ch 上,但是现在这个资源是关闭的。如果还有人需要,我可以和你分享。

对于 Bash 3,有一个特殊的案例有一个很好的简单的解决方案:

如果你不想处理很多变量,或者键只是无效的变量标识符,还有保证你的数组有 少于256项,你可以滥用函数返回值。这个解决方案不需要任何子 shell,因为这个值可以很容易地作为变量使用,也不需要任何迭代,这样性能就会显著提高。而且它非常易读,几乎像 Bash4版本。

下面是最基本的版本:

hash_index() {
case $1 in
'foo') return 0;;
'bar') return 1;;
'baz') return 2;;
esac
}


hash_vals=("foo_val"
"bar_val"
"baz_val");


hash_index "foo"
echo ${hash_vals[$?]}

记住,在 case中使用单引号,否则它会受到 globbing 的影响。从一开始就对静态/冻结哈希非常有用,但是可以从 hash_keys=()数组编写索引生成器。

注意,它默认为第一个元素,所以你可能需要留出第零个元素:

hash_index() {
case $1 in
'foo') return 1;;
'bar') return 2;;
'baz') return 3;;
esac
}


hash_vals=("",           # sort of like returning null/nil for a non existent key
"foo_val"
"bar_val"
"baz_val");


hash_index "foo" || echo ${hash_vals[$?]}  # It can't get more readable than this

注意: 长度现在不正确。

或者,如果你想保留从零开始的索引,你可以保留另一个索引值并防止不存在的键,但它的可读性较低:

hash_index() {
case $1 in
'foo') return 0;;
'bar') return 1;;
'baz') return 2;;
*)   return 255;;
esac
}


hash_vals=("foo_val"
"bar_val"
"baz_val");


hash_index "foo"
[[ $? -ne 255 ]] && echo ${hash_vals[$?]}

或者,为了保持长度正确,偏移索引一:

hash_index() {
case $1 in
'foo') return 1;;
'bar') return 2;;
'baz') return 3;;
esac
}


hash_vals=("foo_val"
"bar_val"
"baz_val");


hash_index "foo" || echo ${hash_vals[$(($? - 1))]}

Shell 没有类似于数据结构的内置映射,我使用原始字符串来描述这样的项:

ARRAY=(
"item_A|attr1|attr2|attr3"
"item_B|attr1|attr2|attr3"
"..."
)

当提取项及其属性时:

for item in "${ARRAY[@]}"
do
item_name=$(echo "${item}"|awk -F "|" '{print $1}')
item_attr1=$(echo "${item}"|awk -F "|" '{print $2}')
item_attr2=$(echo "${item}"|awk -F "|" '{print $3}')


echo "${item_name}"
echo "${item_attr1}"
echo "${item_attr2}"
done

这似乎不比别人的答案聪明,但容易为新人所理解。

迟回复,但是考虑用这种方式解决问题,使用 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

您可以使用动态变量名,并让变量名像散列表的键一样工作。

例如,如果您有一个包含两列的输入文件,名称和信用,如下面的示例所示,并且您希望对每个用户的收入进行求和:

Mary 100
John 200
Mary 50
John 300
Paul 100
Paul 400
David 100

下面的命令将使用动态变量作为键,以 Map _ ${ person }的形式对所有内容进行求和:

while read -r person money; ((map_$person+=$money)); done < <(cat INCOME_REPORT.log)

阅读结果:

set | grep map

产出将是:

map_David=100
map_John=500
map_Mary=150
map_Paul=500

在阐述这些技术时,我正在 GitHub 上开发一个功能,它的工作方式与 HashMap 对象Shell _ map类似。

为了创建“ HashMap 实例”,Shell _ map 函数能够以不同的名称创建自己的副本。每个新的函数副本将有一个不同的 $FUNCNAME 变量。然后使用 $FUNCNAME 为每个 Map 实例创建名称空间。

映射键是全局变量,格式为 $FUNCNAME _ DATA _ $KEY,其中 $KEY 是添加到映射中的键。这些变量是 动态变量

下面我将提供一个简化的版本,以便你可以用作例子。

#!/bin/bash


shell_map () {
local METHOD="$1"


case $METHOD in
new)
local NEW_MAP="$2"


# loads shell_map function declaration
test -n "$(declare -f shell_map)" || return


# declares in the Global Scope a copy of shell_map, under a new name.
eval "${_/shell_map/$2}"
;;
put)
local KEY="$2"
local VALUE="$3"


# declares a variable in the global scope
eval ${FUNCNAME}_DATA_${KEY}='$VALUE'
;;
get)
local KEY="$2"
local VALUE="${FUNCNAME}_DATA_${KEY}"
echo "${!VALUE}"
;;
keys)
declare | grep -Po "(?<=${FUNCNAME}_DATA_)\w+((?=\=))"
;;
name)
echo $FUNCNAME
;;
contains_key)
local KEY="$2"
compgen -v ${FUNCNAME}_DATA_${KEY} > /dev/null && return 0 || return 1
;;
clear_all)
while read var; do
unset $var
done < <(compgen -v ${FUNCNAME}_DATA_)
;;
remove)
local KEY="$2"
unset ${FUNCNAME}_DATA_${KEY}
;;
size)
compgen -v ${FUNCNAME}_DATA_${KEY} | wc -l
;;
*)
echo "unsupported operation '$1'."
return 1
;;
esac
}

用法:

shell_map new credit
credit put Mary 100
credit put John 200
for customer in `credit keys`; do
value=`credit get $customer`
echo "customer $customer has $value"
done
credit contains_key "Mary" && echo "Mary has credit!"

如果 jq 可用,添加另一个选项:

export NAMES="{
\"Mary\":\"100\",
\"John\":\"200\",
\"Mary\":\"50\",
\"John\":\"300\",
\"Paul\":\"100\",
\"Paul\":\"400\",
\"David\":\"100\"
}"
export NAME=David
echo $NAMES | jq --arg v "$NAME" '.[$v]' | tr -d '"'

还有一种非 bash-4(即 bash 3,Mac 兼容)方式:

val_of_key() {
case $1 in
'A1') echo 'aaa';;
'B2') echo 'bbb';;
'C3') echo 'ccc';;
*) echo 'zzz';;
esac
}


for x in 'A1' 'B2' 'C3' 'D4'; do
y=$(val_of_key "$x")
echo "$x => $y"
done

印刷品:

A1 => aaa
B2 => bbb
C3 => ccc
D4 => zzz

使用 case的函数就像一个关联数组。不幸的是,它不能使用 return,所以它必须使用 echo的输出,但这不是问题,除非您是一个纯粹主义者,避免分叉子 shell。