在Bash 3中创建关联数组

42

在彻底搜索了一种在Bash中创建关联数组的方法后,我发现declare -A array可以解决问题。但问题是,它只适用于Bash版本4,而我们系统中服务器所用的Bash版本为3.2.16。

如何在Bash 3中实现类似关联数组的操作?这些值将会被传递给一个脚本,例如:

ARG=array[key];

./script.sh ${ARG}

编辑:我知道可以使用awk或其他工具来完成这个任务,但是我需要在严格的bash环境下解决这个问题。


由于Bash 3已经有了普通数组,所以你只需要基于它们实现关联数组。但是说真的,为什么需要这样做呢? - Kim Stebel
4
请参见BashFAQ/006 - Dennis Williamson
6个回答

37
Bash 3没有关联数组,因此您需要使用其他语言特性来实现您的目的。请注意,在Bash 4下,您编写的代码并不会像您声称的那样工作:./script.sh ${ARG}不会将关联数组传递到子脚本,因为当ARG是关联数组时,${ARG}会扩展为空。您无法将关联数组传递给子进程,因此需要对其进行编码。
您需要在父脚本和子脚本之间定义一些参数传递协议。常见的方法是以key=value的形式传递参数。这假设字符=不出现在键中。
您还需要确定如何在父脚本和子脚本中表示关联数组。它们不需要使用相同的表示方法。
代表关联数组的常见方法是为每个元素使用单独的变量,并使用公共命名前缀。这要求键名仅由ASCII字母(任意大小写)、数字和下划线组成。例如,而不是${myarray[key]},请写成${myarray__key}。如果键是在运行时确定的,则需要先进行一轮扩展:而不是${myarray[$key]},请写成
n=myarray__${key}; echo ${!n}

在一个任务中,使用printf -v。请注意,在printf中使用%s格式来使用指定的值。不要编写printf -v "myarray__${key}" %s "$value",因为这将把$value作为格式处理,并对其执行printf %扩展。

printf -v "myarray__${key}" %s "$value"

如果您需要将一个类似于这样表示的关联数组以key=value参数的形式传递给子进程,您可以使用${!myarray__*}来枚举所有变量,其名称以myarray__开头。

args=()
for k in ${!myarray__*}; do
  n=$k
  args+=("$k=${!n}")
done
在子进程中,将形如key=value的参数转换为带有前缀的单独变量:
for x; do
  if [[ $x != *=* ]]; then echo 1>&2 "KEY=VALUE expected, but got $x"; exit 120; fi
  printf -v "myarray__${x%%=*}" %s "${x#*=}"
done

顺便问一句,你确定这是你需要的吗?与其从另一个bash脚本中调用一个bash脚本,你可能希望在子shell中运行该脚本。这样它将继承父进程的所有变量。


1
你可以使用 printf -v 代替 declare 来避免在函数中局部化变量。而不是调用它,引入子脚本可能也是使父级数组和其他变量对子级可用的一种方法。 - Dennis Williamson
我不明白为什么你在 for k in ``${!myarray__*}``; do 中加入了退格符。这不是一个命令。 - Jocelyn delalande
@Jocelyndelalande 回退??哦,我明白了,是反引号。看起来像是复制粘贴错误,我已经修复了它。 - Gilles 'SO- stop being evil'
你如何直接输出myarray__${key},而不是将其赋值给一个变量? - raine
@rane 我不确定你的意思。你想避免使用临时变量 n 吗?我认为这是不可能的。 - Gilles 'SO- stop being evil'

11
Here is another post/explanation on associative arrays in bash 3 and older using parameter expansion:
https://dev59.com/DHRB5IYBdhLWcg3wNk93#4444841 Gilles' method has a nice if statement to catch delimiter issues, sanitize oddball input ...etc. Use that.
If you are somewhat familiar with parameter expansion:
http://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html To use in your scenario [ as stated: sending to script ]: Script 1: sending_array.sh
# A pretend Python dictionary with bash 3 
ARRAY=( "cow:moo"
        "dinosaur:roar"
        "bird:chirp"
        "bash:rock" )

bash ./receive_arr.sh "${ARRAY[@]}"

脚本2: receive_arr.sh

argAry1=("$@")

function process_arr () {
    declare -a hash=("${!1}")
    for animal in "${hash[@]}"; do
        echo "Key: ${animal%%:*}"
        echo "Value: ${animal#*:}"
    done
}

process_arr argAry1[@]

exit 0

方法2,获取第二个脚本: 脚本1: sending_array.sh

source ./receive_arr.sh
# A pretend Python dictionary with bash 3 
ARRAY=( "cow:moo"
        "dinosaur:roar"
        "bird:chirp"
        "bash:rock" )

process_arr ARRAY[@]

脚本2:receive_arr.sh

function process_arr () {
    declare -a hash=("${!1}")
    for animal in "${hash[@]}"; do
        echo "Key: ${animal%%:*}"
        echo "Value: ${animal#*:}"
    done
}

参考资料:
在bash中将数组作为参数传递


6
如果你不想处理大量的变量,或者键名是无效的变量标识符,并且你的数组保证有小于256个项,那么你可以滥用函数返回值。该解决方案不需要任何子shell,因为该值可以立即作为变量使用,也不需要任何迭代,因此性能非常高。此外,它非常易读,几乎像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[$?]}

有关更多细节和变体,请参见此答案


2

这个其实非常简单。我需要将一个用了很多关联数组的 bash 4 脚本转换成 bash 3。以下两个帮助函数就可以完成全部转换:

array_exp() {
    exp=${@//[/__}
    eval "${exp//]}"
}

array_clear() {
    unset $(array_exp "echo \${!$1__*}")
}

我很惊讶这实际上能够工作,但这就是bash的美妙之处。 例如:

((all[ping_lo] += counts[ping_lo]))

成为

array_exp '((all[ping_lo] += counts[ping_lo]))'

或者这个打印语句:

printf "%3d" ${counts[ping_lo]} >> $return

变得

array_exp 'printf "%3d" ${counts[ping_lo]}' >> $return

唯一需要改变的语法是清除。这样写:
counts=()

成为

array_clear counts

然后您就设置好了。您可以轻松地告诉 array_exp 识别 "=()" 这样的表达式,并通过重写它们为 array_clear 表达式来处理它们,但我更喜欢上述两个函数的简单性。


这看起来很酷。我想知道像 let 'map[$i]++' 这样的语句应该怎么处理,特别是单引号。有什么想法或评论吗? - Ray
在您上面的回答中,当我运行脚本时,array_exp() 中的 eval 语句失败了;它抱怨了一个“坏替换”。 - Ray

2
你可以将键值对写入文件,然后按键进行grep。如果使用类似于以下的模式:
key=value

然后您可以使用egrep寻找以^key=开头的内容,这使得操作非常安全。
要"覆盖"一个值,只需将新值追加到文件末尾,然后使用tail -1获取egrep的最后一个结果即可。
或者,您可以将这些信息放入正常数组中,使用key=value作为数组的值,然后遍历数组以查找值。

0

最终创建了两个数组:

HOST_LIST=(host1 host2)
URL_LIST=(url1 url2)

然后使用索引:

for i in ${!HOST_LIST[@]}; do
    echo ${HOST_LIST[$i]} mapped to ${URL_LIST[$i]}
done

更有用的提取函数:

function GetUrl()
{
    local _HOSTNAME=$1
    local retVal=$2
    for i in ${!HOST_LIST[@]}; do
        if [ "${HOST_LIST[$i]}" = "$_HOSTNAME" ]; then
            eval $retVal="'${URL_LIST[$i]}'"
            return 0
        fi
    done
    return 1
}

被称为:

GetUrl host2 resultUrl
echo $resultUrl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接