Shell 脚本中的关联数组
-
22-08-2019 - |
题
我们需要一个脚本来模拟 Shell 脚本的关联数组或类似 Map 的数据结构,有任何主体吗?
解决方案
要添加到伊尔凡的回答,这里是get()
更短,更快的版本,因为它需要在地图内容不重复:
get() {
mapName=$1; key=$2
map=${!mapName}
value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}
其他提示
另一种选择,如果便携性是不是你主要关注的,是使用内置于外壳关联数组。这应该在bash 4.0的(现在市面上大多数主要发行版,虽然不是在OS X上,除非你自己安装),ksh和zsh的:
declare -A newmap
newmap[name]="Irfan Zulfiqar"
newmap[designation]=SSE
newmap[company]="My Own Company"
echo ${newmap[company]}
echo ${newmap[name]}
根据不同的外壳,你可能需要做一个typeset -A newmap
代替declare -A newmap
的,或者在某些它可能没有必要的。
另一个非bash的4路。
#!/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语句,在那里搜索以及。如果[[$变种=〜/嗒嗒/]]。 或任何。
我认为你需要退一步,想想一个地图,或关联数组,确实是。所有这是存储给定关键字的值,并获得该值重新快速,高效地方式。您可能还希望能够遍历键检索每个键值对,或删除键以及相关的值。
现在,想想你用所有的时间在shell脚本的数据结构,甚至只是没有写一个脚本,具有这些特性的外壳。难倒?它的文件系统。
真的,就需要有在壳编程关联数组是一个临时目录。 mktemp -d
是关联数组构造:
prefix=$(basename -- "$0")
map=$(mktemp -dt ${prefix})
echo >${map}/key somevalue
value=$(cat ${map}/key)
如果你不喜欢使用echo
和cat
,你总是可以写一些小包装;这些的被建模关闭伊尔凡的,尽管他们只是输出的值,而不是设置任意变量,如$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建议的线性搜索更快,以及更坚固(它允许键和值包含 - ,=,空间, QND “:SP:”)。它使用的文件系统不让它放缓的事实;这些文件实际上从来无法保证写入,除非你打电话sync
盘;临时文件像这样寿命短,这不是不可能的,其中许多人将永远不会被写入磁盘。
我做的伊尔凡的代码,伊尔凡的代码杰里的修改,我的代码的一些基准测试,使用下面的驱动程序:
#!/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
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
Bash4支持此本身。不要使用grep
或eval
,它们是最丑陋黑客。
有关与例如代码参见一个冗长,详细的解答: https://stackoverflow.com/questions/3467959
####################################################################
# 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
现在回答这个问题。
以下脚本模拟在外壳脚本关联数组。其简单,很容易理解的。
地图无非是有keyValuePair保存为一个永无止境的字符串 --name =伊尔凡--designation = SSE - 公司=我:SP:乌龙:SP:公司
空间被替换为 ':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"
`
}
有关击3,存在有一个很好的和简单的解决方案的特定情况下:
如果你不想来处理大量的变量,或者键只是无效的变量标识符,和的你的阵列保证具有的小于256项的,你可以滥用函数的返回值。这个解决方案不需要任何亚壳作为值是容易获得的作为一个变量,也没有任何迭代使得性能尖叫。此外,它是非常可读的,几乎像击版本4。
下面是最基础的版本:
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
使用单引号,否则它将受到通配符。从一开始静态/冷冻散列真正有用的,但一个可以写从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))]}
您可以使用动态变量名称,并让变量名称像哈希映射的键一样工作。
例如,如果您有一个包含两列的输入文件,名称、信用,如下例所示,并且您想要对每个用户的收入求和:
Mary 100
John 200
Mary 50
John 300
Paul 100
Paul 400
David 100
下面的命令将使用动态变量作为键对所有内容进行求和,其形式为 地图_${人}:
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函数 能够以不同的名称创建自身的副本。每个新函数副本都将具有不同的 $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!"
可惜我没有看到面前的问题 - 我写的库壳框架一>等含有映射(关联数组)。它的最新版本可以在这里找到 。
示例:
#!/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")"
我已经找到了真正的,前面已经提到的,表现最好的方法是键/丘壑写出到一个文件,然后使用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和模拟为以前的版本)。它被称为壳框架,并在origo.ethz.ch托管但今天的资源被关闭。如果有人仍然需要它,我可以与你分享。
贝壳没有内置地图等数据结构中,我使用的原始字符串来描述这样的项目:
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
这似乎并不比其他人的回答聪明,但很容易理解,为新人们壳。
添加另一种选择,如果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 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的读强>作为代码段内示出从下面一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