Shell脚本中的关联数组


Answers:


20

为了补充Irfan的答案,以下是它的一种更快版本,get()因为它不需要对地图内容进行任何迭代:

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

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

16
分叉子壳和sed几乎不是最佳选择。Bash4本机支持此功能,而bash3具有更好的选择。
lhunath

149

如果不是主要考虑可移植性,则另一个选择是使用内置在外壳中的关联数组。这应该可以在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.0或更高版本的人来说是最好的方法。
Irfan Zulfiqar,2009年

我要添加一点点软糖以确保已设置BASH_VERSION,并且> =4。是的,BASH 4真的非常酷!
蒂姆·波斯特

我正在使用这样的东西。在数组索引/下标不存在的情况下“捕获”错误的最佳方法是什么?例如,如果我将下标作为命令行选项,并且用户输入错误并输入“ designatio”,该怎么办?我收到“错误的数组下标”错误,但在可能的情况下,如何在数组查找时不验证输入?
Jer 2014年

3
@Jer相当晦涩,但是要确定是否在外壳中设置了变量,可以使用test -z ${variable+x}x没关系,可以是任何字符串)。对于Bash中的关联数组,您可以执行类似的操作。使用test -z ${map[key]+x}
Brian Campbell

95

另一种非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语句进行搜索。如果[[$ var =〜/ blah /]]。管他呢。


2
当您确实没有Bash 4时,此方法很好。但是我认为以这种方式获取VALUE的行会更安全:VALUE = $ {animal#*:}。仅使用一个#字符,匹配将停止在第一个“:”上。这也允许值包含“:”。
Ced-le-pingouin 2012年

@ Ced-le-pingouin〜这很重要!我没听懂。我已对帖子进行了修改,以反映您建议的改进。
布本诺夫2012年

1
它使用BASH参数替换对关联数组进行了漂亮的模拟。“键” param-sub替换冒号之前的所有内容,而值模式替换冒号之后的所有内容。类似于正则表达式通配符匹配。所以不是一个真正的关联数组。不推荐使用此方法,除非您需要一种易于理解的方法来在BASH 3或更低版本中执行类似哈希/关联数组的功能。虽然有效!此处更多内容:tldp.org/LDP/abs/html/parameter-substitution.html#PSOREX2
Bubnoff 2014年

1
这没有实现关联数组,因为它没有提供通过键查找项目的方法。它仅提供一种从数字索引中查找每个键(和值)的方法。(可以通过遍历数组来通过键找到一个项目,但这不是关联数组所需要的。)
Eric Postpischil 18/12/22

@EricPostpischil是的。这只是一个hack。它允许一个人在设置中使用熟悉的语法,但是仍然需要像您所说的那样遍历数组。我在以前的评论中试图明确指出,它绝对不是关联数组,如果您有其他选择,我什至不建议这样做。在我看来,它唯一对它有利的一点是,对于熟悉Python等其他语言的人来说,它易于编写和使用。如果您确实要在BASH 3中实现关联数组,那么您可能需要稍微追溯一下步骤。
布本诺夫

34

我认为您需要退后一步,想一想什么是地图或关联数组。这是一种存储给定键的值并快速有效地取回该值的方法。您可能还希望能够遍历键,以检索每个键值对,或删除键及其相关值。

现在,考虑一下您在Shell脚本中一直使用的数据结构,甚至在没有编写脚本的情况下仅在Shell中使用的具有这些属性的数据结构。难过吗 它是文件系统。

确实,在shell编程中要有一个关联数组,您需要的只是一个temp目录。mktemp -d是您的关联数组构造函数:

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进行线性搜索快得多,并且更加健壮(它允许键和值包含-,=,空格,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
    用户0m0.280s
    sys 0m0.691s

    $ time ./driver.sh brian 10 5

    真正的0m0.226s
    用户0m0.057s
    sys 0m0.123s

    $ time ./driver.sh杰里10 5

    真正的0m0.706s
    用户0m0.228s
    sys 0m0.530s

    $ time ./driver.sh irfan 100 5

    真正的0m10.633s
    用户0m4.366s
    sys 0m7.127s

    $ time ./driver.sh布莱恩100 5

    真正的0m1.682s
    用户0m0.546s
    sys 0m1.082s

    $ time ./driver.sh杰瑞100 5

    真正的0m9.315s
    用户0m4.565s
    sys 0m5.446s

    $ time ./driver.sh irfan 10 500

    真实1m46.197s
    用户0m44.869s
    SYS 1分12.282秒

    $ time ./driver.sh布莱恩10 500

    真正的0m16.003s
    用户0m5.135s
    sys 0m10.396s

    $ time ./driver.sh杰里10500

    真正的1m24.414s
    用户0m39.696s
    sys 0分54.834秒

    $ time ./driver.sh irfan 1000 5

    真正的4m25.145s
    用户3m17.286s
    sys 1分21.490秒

    $ time ./driver.sh brian 1000 5

    真正的0m19.442s
    用户0m5.287s
    sys 0分10.751秒

    $ time ./driver.sh杰里1000 5

    真实5m29.136s
    用户4m48.926s
    sys 0m59.336s


1
我不认为您应该为地图使用文件系统,基本上是将IO用于可以在内存中快速完成的事情。
Irfan Zulfiqar,2009年

9
文件不一定会被写入磁盘。除非您调用同步,否则操作系统可能只是将它们保留在内存中。您的代码正在调用sed并进行了几次线性搜索,这些搜索都很慢。我做了一些快速基准测试,而我的版本速度提高了5-35倍。
布赖恩·坎贝尔

另一方面,bash4的本机数组是一种更好的方法,在bash3中,您仍然可以将所有内容保留在磁盘上,而无需使用声明和间接调用。
lhunath

7
无论如何,“ fast”和“ shell”并没有真正并存:肯定不是因为我们在“避免小型IO”级别讨论的速度问题。您可以搜索并使用/ dev / shm来保证没有IO。
2011年

2
这个解决方案令我惊讶,而且很棒。在2016年仍然适用。这确实应该是公认的答案。
Gordon


7
####################################################################
# 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

4

现在回答这个问题。

以下脚本在Shell脚本中模拟关联数组。它简单易懂。

Map就是一个永无休止的字符串,它的keyValuePair保存为--name = Irfan --designation = 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"
          `
}

1
您正在eval处理数据,就好像它是bash代码一样,而且:无法正确引用数据。两者都导致大量的错误和任意代码注入。
lhunath

3

对于Bash 3,有一个特殊的情况,它具有很好的解决方案:

如果您不想处理很多变量,或者键只是无效的变量标识符,并且您的数组保证少于256个项目,则可以滥用函数返回值。该解决方案不需要任何子外壳,因为该值很容易作为变量使用,也不需要任何迭代就可以降低性能。而且它非常易读,几乎像Bash 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))]}

2

您可以使用动态变量名称,并让变量名称像哈希图的键一样工作。

例如,如果您有一个包含两列的输入文件,例如name,credit,如下所示,并且您想对每个用户的收入求和:

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 Objectshell_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!"

2

另一种非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输出,但这不是问题,除非您是一个精通派生subshel​​l的纯粹主义者。


1

可惜我之前没有看到这个问题-我写了一个库shell-framework,其中包含了maps(关联数组)。它的最新版本可以在这里找到。

例:

#!/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")"

1

如果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 '"' 

0

如前所述,我发现确实最好的方法是将键/值写到文件中,然后使用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操作。


0

几年前,我为bash编写了脚本库,该库在其他功能(日志记录,配置文件,对命令行参数的扩展支持,生成帮助,单元测试等)中支持关联数组。该库包含一个用于关联数组的包装器,并自动切换到适当的模型(对于bash4是内部模型,对于以前的版本是仿真)。它称为shell框架,托管在origo.ethz.ch上,但是今天资源已关闭。如果有人仍然需要它,我可以与您分享。


可能值得将其粘贴在github上
Mark K Cowan

0

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

这似乎并不比其他人的答案聪明,但对于新手来说却很容易理解。


-1

我用以下方法修改了Vadim的解决方案:

####################################################################
# 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进行更改,以防止在您请求不存在的键时返回错误,尽管其副作用是它也将默默地忽略丢失的地图,但由于我只是想检查一个键,以便跳过循环中的项目。


-1

回复较晚,但是考虑使用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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.