我们需要一个能够模拟关联数组或类似于映射数据结构的脚本,用于Shell脚本编程。请问有人知道如何实现吗?
我们需要一个能够模拟关联数组或类似于映射数据结构的脚本,用于Shell脚本编程。请问有人知道如何实现吗?
如果可移植性不是您的主要问题,另一个选择是使用内置到 shell 中的关联数组。这应该适用于 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]}
根据所使用的shell,你可能需要执行typeset -A newmap
而不是declare -A newmap
,或者在某些情况下可能完全不需要。
test -z ${variable+x}
(这里的x
并不重要,可以是任何字符串)。对于Bash中的关联数组,你也可以采用类似的方式:使用test -z ${map[key]+x}
来判断是否已经设置了某个键值。 - Brian Campbelldeclare -a newmap
。 - harshainfovar2=${newmap[$mykey]}
。 - Aditya Kashi另外一种非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语句进行搜索。例如:if [[ $var =~ /blah/ ]]. 或者其他什么的。
我认为你需要退后一步,思考一下映射或关联数组的本质。它只是一种存储给定键的值,并快速有效地获取该值的方式。您可能还希望能够迭代键以检索每个键值对,或者删除键及其关联值。
现在,想想你在 shell 脚本中经常使用的数据结构,甚至在没有编写脚本的情况下在 shell 中使用,具有这些属性的数据结构是什么?卡住了吧?那就是文件系统。
实际上,在 shell 编程中拥有关联数组所需的只是一个临时目录。mktemp -d
就是您的关联数组构造函数:
prefix=$(basename -- "$0")
map=$(mktemp -dt ${prefix})
echo >${map}/key somevalue
value=$(cat ${map}/key)
如果您不想使用echo
和cat
,您可以编写一些小的包装器;这些包装器是模仿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进行线性搜索要快得多,而且更加健壮(它允许键和值包含“-”、“=”、“空格”、“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
真实时间:0m0.975s 用户态CPU时间:0m0.280s 系统态CPU时间:0m0.691s
$ time ./driver.sh brian 10 5
真实时间:0m0.226s 用户态CPU时间:0m0.057s 系统态CPU时间:0m0.123s
$ time ./driver.sh jerry 10 5
真实时间:0m0.706s 用户态CPU时间:0m0.228s 系统态CPU时间:0m0.530s
$ time ./driver.sh irfan 100 5
真实时间:0m10.633s 用户态CPU时间:0m4.366s 系统态CPU时间:0m7.127s
$ time ./driver.sh brian 100 5
真实时间:0m1.682s 用户态CPU时间:0m0.546s 系统态CPU时间:0m1.082s
$ time ./driver.sh jerry 100 5
真实时间:0m9.315s 用户态CPU时间:0m4.565s 系统态CPU时间:0m5.446s
$ time ./driver.sh irfan 10 500
真实时间:1m46.197s 用户态CPU时间:0m44.869s 系统态CPU时间:1m12.282s
$ time ./driver.sh brian 10 500
真实时间:0m16.003s 用户态CPU时间:0m5.135s 系统态CPU时间:0m10.396s
$ time ./driver.sh jerry 10 500
真实时间:1m24.414s 用户态CPU时间:0m39.696s 系统态CPU时间:0m54.834s
$ time ./driver.sh irfan 1000 5
真实时间:4m25.145s 用户态CPU时间:3m17.286s 系统态CPU时间:1m21.490s
$ time ./driver.sh brian 1000 5
真实时间:0m19.442s 用户态CPU时间:0m5.287s 系统态CPU时间:0m10.751s
$ time ./driver.sh jerry 1000 5
真实时间:5m29.136s 用户态CPU时间:4m48.926s 系统态CPU时间:0m59.336s
在Irfan的答案的基础上,这里提供了一个更短更快的get()
版本,因为它不需要对映射内容进行迭代:
get() {
mapName=$1; key=$2
map=${!mapName}
value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}
以下是另一种非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的纯粹主义者。
####################################################################
# 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
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))]}
hash_index "foo" || echo ${hash_vals[$(($? - 1))]}
赋值给一个变量,那么正确的代码应该是 output="foo" || echo ${hash_vals[$(($? - 1))]}
。如果您不介意的话,能否告诉我正确的赋值方式。 - Bowen Pengput() {
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"
`
}
eval
操作,更糟糕的是:你没有正确地引用它。这两个问题都会导致大量的错误和任意代码注入。 - lhunathMary 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
#!/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!"