如何保持关联数组的顺序?

49

我尝试在Bash中迭代遍历一个关联数组。

这似乎很简单,但是循环不会按照数组的初始顺序进行。

以下是一个简单的脚本可以尝试:

#!/bin/bash

echo -e "Workspace\n----------";
lsb_release -a

echo -e "\nBash version\n----------";
echo -e $BASH_VERSION."\n";

declare -A groups;
groups["group1"]="123";
groups["group2"]="456";
groups["group3"]="789";
groups["group4"]="abc";
groups["group5"]="def";

echo -e "Result\n----------";
for i in "${!groups[@]}"
do
    echo "$i => ${groups[$i]}";
done

输出:

Workspace
----------
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.2 LTS
Release:    14.04
Codename:   trusty

Bash version
----------
4.3.11(1)-release.

Result
----------
group3 => 789
group2 => 456
group1 => 123
group5 => def
group4 => abc

为什么我没有 group1group2 等等?

我不想按字母数字顺序,我希望循环遵循数组最初声明的顺序...

有办法吗?


1
Bash中的关联数组是无序的,因此如果您希望有顺序,那么也许应该使用不同的数据类型? - Rufflewind
5个回答

63

正如先前指出的,这里没有错误。关联数组是按“哈希”顺序存储的。如果您需要排序,请不要使用关联数组。或者,您可以同时使用非关联数组和关联数组。

保留第二个(非关联)数组,它按创建顺序标识键。然后遍历第二个数组,使用其内容作为键来打印数据时,访问第一个(关联)数组。像这样:

declare -A groups;      declare -a orders;
groups["group1"]="123"; orders+=( "group1" )
groups["group2"]="456"; orders+=( "group2" )
groups["group3"]="789"; orders+=( "group3" )
groups["group4"]="abc"; orders+=( "group4" )
groups["group5"]="def"; orders+=( "group5" )

# Convoluted option 1
for i in "${!orders[@]}"
do
    echo "${orders[$i]}: ${groups[${orders[$i]}]}"
done
echo

# Convoluted option 1 - 'explained'
for i in "${!orders[@]}"
do
    echo "$i: ${orders[$i]}: ${groups[${orders[$i]}]}"
done
echo

# Simpler option 2 - thanks, PesaThe
for i in "${orders[@]}"
do
    echo "$i: ${groups[$i]}"
done

'简单的方案2'是由PesaThe评论中提出的,应该优先使用该方案,而不是'复杂的方案'。

group1: 123
group2: 456
group3: 789
group4: abc
group5: def

0: group1: 123
1: group2: 456
2: group3: 789
3: group4: abc
4: group5: def

group1: 123
group2: 456
group3: 789
group4: abc
group5: def

你可能不想像那样每行写两个语句,但这样强调了处理这两个数组之间的并行性。

在问题中,赋值后面的分号并不是真正必要的(虽然它们没有实际伤害,除了让读者想知道“为什么?”)。


谢谢,实现这个的最聪明方式 :) - Doubidou
这是唯一明智的方式来完成这个任务。 - sjas
4
像这样循环有没有不推荐的理由呢?for i in "${orders[@]}"; do echo "$i: ${groups[$i]}"; done - PesaThe
1
@PesaThe:不,我认为没有不使用您更简单符号的好理由。我认为这是被问题中的符号误导,并且没有足够努力地简化它的情况。感谢您的建议! - Jonathan Leffler
1
请使用declare -a orders=();来声明数组,否则每次使用orders+=时都会附加整个数组数据。 - MJK
@MJK 这是不必要的。Bash手册(v4.4.20)中指出:"当使用复合赋值(参见下面的数组)将+=应用于数组变量时,变量的值不会被取消设置(与使用=时相反),并且新值将附加到数组开始的最大索引(对于索引数组)的下一个位置..." - HQW.ang

11

我的方法是首先创建一个按键排序的数组:

keys=( $( echo ${!dict[@]} | tr ' ' $'\n' | sort ) )
for k in ${keys[@]}; do
    echo "$k=${dict[$k]}"
done

如果你能以某种方式对键进行排序,那就很好用。 - rogerdpack
只有在键是可排序的情况下,Indeed才能正常工作。无法按照键的“声明”顺序进行迭代。 - undefined

6

另一种在关联数组中对条目进行排序的方法是将组列表作为关联数组中添加的一个条目进行保存。将此条目的键称为“group_list”。每添加一个新组,都将其附加到group_list字段中,并添加一个空格以分隔后续添加。这是我为一个名为master_array的关联数组所做的:

master_array["group_list"]+="${new_group}";

要按照添加顺序对组进行排序,可以使用for循环遍历group_list字段,然后可以在关联数组中访问组字段。这里是我为master_array编写的代码片段:

for group in ${master_array["group_list"]}; do
    echo "${group}";
    echo "${master_array[${group},destination_directory]}";
done

以下是该代码的输出结果:

"linux"
"${HOME}/Backup/home4"
"data"
"${HOME}/Backup/home4/data"
"pictures"
"${HOME}/Backup/home4/pictures"
"pictures-archive"
"${HOME}/Backup/home4/pictures-archive"
"music"
"${HOME}/Backup/home4/music"

这类似于Jonathan Leffler的建议,但是将数据与关联数组一起保留,而不需要保留两个单独的不相关的数组。正如您所看到的,它不是随机顺序,也不是按字母顺序排列,而是我添加它们到数组中的顺序。
另外,如果您有子组,您可以为每个组创建子组列表,并通过这些列表进行排序。这就是我这样做的原因,旨在减轻访问关联数组所需的多个数组的需求,并且允许扩展到新的子组而无需修改代码。
编辑:修正了一些拼写错误。

1

这里有一些人体工程学抽象:

add() {
  local var=$1
  local key=$2
  local val=$3
  declare -ga "${var}_ORDER"
  declare -gA "${var}_MAP"
  local -n map=${var}_MAP
  local -n order=${var}_ORDER
  order+=("$key")
  map["$key"]=$val
}

add groups group1 123
add groups group2 456
add groups group3 789
add groups group4 abc
add groups group5 def

get() {
  local var=$1
  local key=$2
  local -n tmp=${var}_MAP
  echo "${tmp[$key]}"
}

get groups group2 # 456
get groups group5 # def

keys() {
  local var=$1
  local -n tmp=${var}_ORDER
  echo "${tmp[@]}"
}

keys groups # group1 group2 group3 group4 group5

values() {
  local var=$1
  local -n map=${var}_MAP
  local -n order=${var}_ORDER
  for k in "${order[@]}"; do
    echo -n "${map[$k]} "
  done
  echo
}

values groups # 123 456 789 abc def

for i in $(keys groups); do
  echo "$i => $(get groups "$i")"
done

# group1 => 123
# group2 => 456
# group3 => 789
# group4 => abc
# group5 => def

0
这里有一些可以直接使用的函数来实现这个目的。基于@joe-hillenbrand的版本,这段代码不太可能触发命名空间冲突(由于declare -n),并且提供了一个原生的数组键循环,没有子shell调用和潜在的引用问题(等等)。它还正确处理了两次添加相同键(保留原始顺序)。用法:
# avoid shellcheck warnings by declaring first.
declare myhash
hash_init myhash

hash_set myhash key1 val1
hash_set myhash key2 val2
hash_append myhash key1 ' appending works'

declare val
for key in "${myhash[@]}"; do
    hash_get myhash "$key" val
    echo "key »$key« val »$val«"
done

hash_key_exists myhash "key_not_set"; echo "key_is set: $?"
# throws error
hash_get myhash "key_not_set" val
echo "key_not_set »$val«"

代码:
# $1 map
hash_init(){
    declare -ga "$1"
    declare -gA "_hash_${1}_map"
}

# $1 map, $2 key, $3 value
# Setting an existing value does not change its order.
hash_set(){
    _hash_set "$@" false
}

# $1 map, $2 key, $3 value
hash_append(){
    _hash_set "$@" true
}

# $1 map, $2 key, $3: named parameter to be filled
hash_get(){
    local -n _hash_get_map
    local -n _hash_get_key
    _hash_get_map="_hash_${1}_map"
    _hash_get_key="$3"
    _hash_get_key="${_hash_get_map["$2"]?$2 is unset}"

}

# $1 map, $2 key
hash_key_exists(){
    local -n _hash_key_exists_map
    _hash_key_exists_map="_hash_${1}_map"
    [[ -n ${_hash_key_exists_map["$2"]+x} ]]
}

# Private member $1 map, $2 key, $3 value, $4 append=true
_hash_set(){
    local -n _hash_set_arr
    local -n _hash_set_map
    _hash_set_arr="$1"
    _hash_set_map="_hash_${1}_map"
    [[ -n ${_hash_set_map["$2"]+x} ]] || _hash_set_arr+=("$2")
    if [[ "$4" == true ]]; then
        if [[ -z ${_hash_set_map["$2"]+x} ]]; then
            echo "${FUNCNAME[1]}: cannot append »$3« to non-existing key »$2«" >&2
            return 1
        fi
        # append it
        _hash_set_map["$2"]+="$3"
    else
        # set it
        _hash_set_map["$2"]="$3"
    fi
}


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