【问题标题】:Multi-dimensional arrays in BashBash 中的多维数组
【发布时间】:2012-06-29 08:01:55
【问题描述】:

我正在计划一个脚本来管理我的 Linux 系统的某些部分,并且正在决定是使用 还是

我更喜欢将其作为 Bash 脚本执行,因为命令更简单,但真正的决定因素是配置。我需要能够在配置文件中存储一个多维数组来告诉脚本如何处理自己。使用 bash 在配置文件中存储简单的 key=value 对很容易,但我能想到的唯一方法是做一个多维数组是一个两层解析引擎,比如

array=&d1|v1;v2;v3&d2|v1;v2;v3

但是 marshall/unmarshall 代码可能会变成熊,对于下一个必须管理它的可怜的 sap 来说,它远非用户友好。如果我不能在 bash 中轻松做到这一点,我只需将配置写入 xml 文件并在 python 中编写脚本。

在 bash 中有没有简单的方法来做到这一点?

谢谢大家。

【问题讨论】:

标签: bash python arrays bash shell multidimensional-array


【解决方案1】:

Bash 不支持多维数组,也不支持散列,而且您似乎想要一个值是数组的散列。这个解决方案不是很漂亮,有xml文件的解决方案应该更好:

array=('d1=(v1 v2 v3)' 'd2=(v1 v2 v3)')
for elt in "${array[@]}";do eval $elt;done
echo "d1 ${#d1[@]} ${d1[@]}"
echo "d2 ${#d2[@]} ${d2[@]}"

编辑:这个答案已经很老了,因为 bash 4 支持哈希表,另请参阅 this answer 了解没有 eval 的解决方案。

【讨论】:

  • 这就是我所期望的答案,但它从来没有心去问。谢谢
  • 只是一个注释。 bash 从版本 4 开始支持哈希(关联数组)。更多信息:mywiki.wooledge.org/BashFAQ/006
  • FYI 关联数组可以设置一些“声明”属性,例如“赋值时设置为大写”,但是,它们不能有 -A [关联] 或 -a [数字]数组设置为它们,也不是 -n 引用集,但您可以用数字标记变量名称,并在适当位置使用变量:MYARR_$i_[$j] 将是最接近此的东西,但它不是真的md 数组,但这是你将得到的最好的。如果您足够绝望,您也可以将函数用作伪数组系统:)
  • 它是……邪恶的……对不起,我的意思是评估
  • 你总是可以用declare -a "$elt" 代替eval $elt
【解决方案2】:

Bash 没有多维数组。但是您可以使用关联数组来模拟类似的效果。下面是一个关联数组伪装成多维数组的例子:

declare -A arr
arr[0,0]=0
arr[0,1]=1
arr[1,0]=2
arr[1,1]=3
echo "${arr[0,0]} ${arr[0,1]}" # will print 0 1

如果您不将数组声明为关联数组(使用-A),上述方法将不起作用。例如,如果省略declare -A arr 行,echo 将打印2 3 而不是0 1,因为0,01,0 等将被视为算术表达式并计算为0 (逗号运算符右侧的值)。

【讨论】:

  • declare -A [arrayname] 引发一个“无效选项”,至少在 OS X 中是这样。
  • 编辑:看起来 OS X 带有 bash 3,这个选项需要 bash 4。谢谢,Apple。
【解决方案3】:

这要归功于 1. “间接扩展” !它增加了一层间接性,以及 2.“子字符串扩展”,它与数组的行为不同,可用于“切片”它们,如 https://stackoverflow.com/a/1336245/317623

所述
# Define each array and then add it to the main one
SUB_0=("name0" "value 0")
SUB_1=("name1" "value;1")
MAIN_ARRAY=(
  SUB_0[@]
  SUB_1[@]
)

# Loop and print it.  Using offset and length to extract values
COUNT=${#MAIN_ARRAY[@]}
for ((i=0; i<$COUNT; i++))
do
  NAME=${!MAIN_ARRAY[i]:0:1}
  VALUE=${!MAIN_ARRAY[i]:1:1}
  echo "NAME ${NAME}"
  echo "VALUE ${VALUE}"
done

它基于this answer here

【讨论】:

  • 它应该是 NAME=${!MAIN_ARRAY[i]:0:1}VALUE=${!MAIN_ARRAY[i]:1:1},如果我没记错的话(即交换 URLSMAIN)。
  • 我还没有调查原因 - 但这在 bash v4.3+ 上运行良好,但在早期版本中存在问题。 (打印单个字符而不是完整的单词)
  • 我希望 with 可以工作,而不必在 MAIN_ARRAY 之前定义 SUB_0SUB_1
  • @dtmland 旧 bash 版本似乎发生的是普通子字符串扩展,而不是数组切片替代方案。
【解决方案4】:

如果您想使用 bash 脚本并使其易于阅读,建议将数据放入结构化 JSON 中,然后在 bash 命令中使用 lightweight tool jq 来遍历数组。例如以下数据集:

[

    {"specialId":"123",
    "specialName":"First"},

    {"specialId":"456",
    "specialName":"Second"},

    {"specialId":"789",
    "specialName":"Third"}
]

您可以像这样使用 bash 脚本和 jq 遍历这些数据:

function loopOverArray(){

    jq -c '.[]' testing.json | while read i; do
        # Do stuff here
        echo "$i"
    done
}

loopOverArray

输出:

{"specialId":"123","specialName":"First"}
{"specialId":"456","specialName":"Second"}
{"specialId":"789","specialName":"Third"}

【讨论】:

  • 谢谢你。绝对是最简单的实用解决方案。我们可以把纯洁留给纯粹主义者。
【解决方案5】:

独立于所使用的外壳(sh、ksh、bash、...),以下方法非常适用于 n 维数组(示例涵盖二维数组)。

在示例中,行分隔符(第一维)是空格字符。为了引入字段分隔符(第二维),使用标准 unix 工具 tr。附加维度的附加分隔符可以以相同的方式使用。

当然这种方法的性能不是很好,但是如果性能不是一个标准,这种方法是相当通用的,可以解决很多问题:

array2d="1.1:1.2:1.3 2.1:2.2 3.1:3.2:3.3:3.4"

function process2ndDimension {
    for dimension2 in $*
    do
        echo -n $dimension2 "   "
    done
    echo
}

function process1stDimension {
    for dimension1 in $array2d
    do
        process2ndDimension `echo $dimension1 | tr : " "`
    done
}

process1stDimension

该样本的输出如下所示:

1.1     1.2     1.3     
2.1     2.2     
3.1     3.2     3.3     3.4 

【讨论】:

  • 如果值包含空格或冒号,则效果不佳。 :)
  • @dannysauer - 如果您将空格和冒号编码为实体或十六进制代码,应该没问题。例如 %20 和 %3a。如果您担心的话,通过这些函数处理数组的输入和输出应该很容易。
  • 那么这些值不包含空格或冒号。 :D
  • 我可以有一个循环和一个函数而不是两个吗?
【解决方案6】:

经过大量试验和错误,我实际上发现 bash 上最好、最清晰和最简单的多维数组是使用常规 var。是的。

优点:你不必遍历一个大数组,你可以直接回显 "$var" 并使用 grep/awk/sed。它简单明了,您可以拥有任意数量的列。

例子:

$ var=$(echo -e 'kris hansen oslo\nthomas jonson peru\nbibi abu johnsonville\njohnny lipp peru')

$ echo "$var"
kris hansen oslo
thomas johnson peru
bibi abu johnsonville
johnny lipp peru

如果你想找到秘鲁的每一个人

$ echo "$var" | grep peru
thomas johnson peru
johnny lipp peru

仅在第三个字段中使用 grep(sed)

$ echo "$var" | sed -n -E '/(.+) (.+) peru/p'
thomas johnson peru
johnny lipp peru

如果你只想要 x 字段

$ echo "$var" | awk '{print $2}'
hansen
johnson
abu
johnny

秘鲁每个叫 thomas 的人都返回他的姓氏

$ echo "$var" |grep peru|grep thomas|awk '{print $2}'
johnson

任何你能想到的查询...超级简单。

要更改项目:

$ var=$(echo "$var"|sed "s/thomas/pete/")

删除包含“x”的行

$ var=$(echo "$var"|sed "/thomas/d")

根据另一个项目的值更改同一行中的另一个字段

$ var=$(echo "$var"|sed -E "s/(thomas) (.+) (.+)/\1 test \3/")
$ echo "$var"
kris hansen oslo                                                                                                                                             
thomas test peru                                                                                                                                          
bibi abu johnsonville
johnny lipp peru

当然循环也可以,如果你想这样做

$ for i in "$var"; do echo "$i"; done
kris hansen oslo
thomas jonson peru
bibi abu johnsonville
johnny lipp peru

我发现的唯一问题是你必须总是引用 var(在示例中;var 和 i) 或者事情看起来像这样

$ for i in "$var"; do echo $i; done
kris hansen oslo thomas jonson peru bibi abu johnsonville johnny lipp peru

毫无疑问,有人会说如果您的输入中有空格,这将不起作用,但是可以通过在输入中使用另一个分隔符来解决此问题,例如(现在使用 utf8 字符来强调您可以选择输入的内容不会包含,但你可以选择任何ofc):

$ var=$(echo -e 'field one☥field two hello☥field three yes moin\nfield 1☥field 2☥field 3 dsdds aq')

$ for i in "$var"; do echo "$i"; done
field one☥field two hello☥field three yes moin
field 1☥field 2☥field 3 dsdds aq

$ echo "$var" | awk -F '☥' '{print $3}'
field three yes moin
field 3 dsdds aq

$ var=$(echo "$var"|sed -E "s/(field one)☥(.+)☥(.+)/\1☥test☥\3/")
$ echo "$var"
field one☥test☥field three yes moin
field 1☥field 2☥field 3 dsdds aq

如果您想在输入中存储换行符,您可以在输入之前将换行符转换为其他内容,然后在输出中再次将其转换回(或者不要使用 bash...)。尽情享受吧!

【讨论】:

  • 这可能适用于二维,但对于具有 n 维的通用情况,这变得越来越难。
  • 确实如此。当我写这篇文章时,我没有意识到 2d、3d、4d 之间的区别,而我所需要的只是良好的表格支持/db。现在,我仍然不需要超过 2d 的东西,但如果需要更多,我想这将行不通。
【解决方案7】:

我发布以下内容是因为它是一种非常简单明了的方式来模拟(至少在某种程度上)Bash 中二维数组的行为。它使用一个 here-file(参见 Bash 手册)和 read(一个 Bash 内置命令):

## Store the "two-dimensional data" in a file ($$ is just the process ID of the shell, to make sure the filename is unique)
cat > physicists.$$ <<EOF
Wolfgang Pauli 1900
Werner Heisenberg 1901
Albert Einstein 1879
Niels Bohr 1885
EOF
nbPhysicists=$(wc -l physicists.$$ | cut -sf 1 -d ' ')     # Number of lines of the here-file specifying the physicists.

## Extract the needed data
declare -a person     # Create an indexed array (necessary for the read command).                                                                                 
while read -ra person; do
    firstName=${person[0]}
    familyName=${person[1]}
    birthYear=${person[2]}
    echo "Physicist ${firstName} ${familyName} was born in ${birthYear}"
    # Do whatever you need with data
done < physicists.$$

## Remove the temporary file
rm physicists.$$

输出: Physicist Wolfgang Pauli was born in 1900 Physicist Werner Heisenberg was born in 1901 Physicist Albert Einstein was born in 1879 Physicist Niels Bohr was born in 1885

工作方式:

  • 创建的临时文件中的行扮演一维向量的角色,其中空格(或您选择的任何分隔字符;请参阅 Bash 手册中read 命令的描述)分隔这些元素的元素向量。
  • 然后,使用带有-a 选项的read 命令,我们循环遍历文件的每一行(直到到达文件末尾)。对于每一行,我们可以将所需的字段(= 单词)分配给我们在循环之前声明的数组。 read 命令的 -r 选项可防止反斜杠充当转义字符,以防我们在此处的文档 physicists.$$ 中键入反斜杠。

总之,一个文件被创建为一个二维数组,它的元素是通过每行循环提取的,并使用read 命令的能力将单词分配给(索引)数组的元素。

略有改善:

在上面的代码中,文件physicists.$$ 作为while 循环的输入,因此它实际上被传递给read 命令。但是,我发现当我在 while 循环中有另一个命令要求输入时,这会导致问题。例如,select 命令等待标准输入,如果放在while 循环中,它将从physicists.$$ 获取输入,而不是在命令行中提示用户输入。 为了纠正这个问题,我使用了read-u 选项,它允许从文件描述符中读取。我们只需要创建一个与physicists.$$ 对应的文件描述符(使用exec 命令),并将其提供给读取的-u 选项,如下代码所示:

## Store the "two-dimensional data" in a file ($$ is just the process ID of the shell, to make sure the filename is unique)
cat > physicists.$$ <<EOF
Wolfgang Pauli 1900
Werner Heisenberg 1901
Albert Einstein 1879
Niels Bohr 1885
EOF
nbPhysicists=$(wc -l physicists.$$ | cut -sf 1 -d ' ')     # Number of lines of the here-file specifying the physicists.
exec {id_file}<./physicists.$$     # Create a file descriptor stored in 'id_file'.

## Extract the needed data
declare -a person     # Create an indexed array (necessary for the read command).                                                                                 
while read -ra person -u "${id_file}"; do
firstName=${person[0]}
familyName=${person[1]}
birthYear=${person[2]}
echo "Physicist ${firstName} ${familyName} was born in ${birthYear}"
# Do whatever you need with data
done

## Close the file descriptor
exec {id_file}<&-
## Remove the temporary file
rm physicists.$$

注意文件描述符最后关闭了。

【讨论】:

  • 和user7909577的解决方案非常相似。但是,您所做的是在文本文件中写入一系列行。所以维度1是行号。维度 2 是一行中的单词。然而,一个真正通用的解决方案将允许将任何数组放入任何数组中。那是行不通的。好的,您可以通过缩进线条来实现尺寸,但这仍然不是很优雅。
  • @U. Windi“将任何数组放在任何数组中。那是行不通的。”:这就是为什么我在回答中强调它是模拟 2D-ARRAYS。 “这与user7909577的解决方案非常相似”:我不这么认为,他/她在他的答案中用sed / grep / awk解析一个字符串,我先将字符串“转换”为数组,然后使用索引(这就是它模拟数组行为的原因)。
  • @Guiseppe:但两种解决方案都使用文本文件作为二维数组的表示形式,其维度如我所描述。这个问题具体是关于 multi 维数组(对于二维数组有一个单独的问题)。
  • @U. Windl:我同意,但据我了解 user7909577 的解决方案,它从来都不是你真正拥有的数组,而是一个长长的列表,你必须在其中放置分隔符(如换行符)。所以每次你开始一个新的循环时,你都会被你的长列表中的一个元素所困。与我的解决方案相反,在每个循环中,您都使用一个真正的一维数组(模拟二维数组的一行),因此您可以访问它的任何元素,这在我看来要方便得多。我没有注意到关于二维数组的其他问题,所以如果你知道如何移动答案我很感兴趣。
【解决方案8】:

扩展 Paul 的答案 - 这是我在 bash 中使用关联子数组的版本:

declare -A SUB_1=(["name1key"]="name1val" ["name2key"]="name2val")
declare -A SUB_2=(["name3key"]="name3val" ["name4key"]="name4val")
STRING_1="string1val"
STRING_2="string2val"
MAIN_ARRAY=(
  "${SUB_1[*]}"
  "${SUB_2[*]}"
  "${STRING_1}"
  "${STRING_2}"
)
echo "COUNT: " ${#MAIN_ARRAY[@]}
for key in ${!MAIN_ARRAY[@]}; do
    IFS=' ' read -a val <<< ${MAIN_ARRAY[$key]}
    echo "VALUE: " ${val[@]}
    if [[ ${#val[@]} -gt 1 ]]; then
        for subkey in ${!val[@]}; do
            subval=${val[$subkey]}
            echo "SUBVALUE: " ${subval}
        done
    fi
done

它适用于主数组中的混合值 - 字符串/数组/关联。数组

这里的关键是将子数组用单引号括起来,并在将子数组存储在主数组中时使用* 而不是@,以便将其存储为单个空格分隔的字符串:"${SUB_1[*]}"

然后,在使用 IFS=' ' read -a val &lt;&lt;&lt; ${MAIN_ARRAY[$key]} 循环遍历值时,可以很容易地从中解析出一个数组

上面的代码输出:

COUNT:  4
VALUE:  name1val name2val
SUBVALUE:  name1val
SUBVALUE:  name2val
VALUE:  name4val name3val
SUBVALUE:  name4val
SUBVALUE:  name3val
VALUE:  string1val
VALUE:  string2val

【讨论】:

  • 因此您不能存储包含空格的字符串(更不用说换行符)。此外,您缺少引号也无法安全地使用 glob 字符。
  • 是的,我刚刚意识到它会将带有空格的字符串分解为单独的子值,但如果它只是配置值,我个人可以忍受......但我认为这是关于我们可以在 bash 中做的最好的事情。不过,我愿意接受建议/改进。添加了引号,谢谢指出:)
  • 您好,您能解释一下IFS=' ' read -a val &lt;&lt;&lt; ${MAIN_ARRAY[$key]} 的工作原理吗?什么是 IFS?它是在那里定义的变量吗?为什么 read 会附加到变量定义中?
  • @CharlesW。请参阅Bash Reference Manual, 5.1 Bourne Shell Variables:“IFS – 分隔字段的字符列表;当 shell 将单词拆分为扩展的一部分时使用。”和6.7 Arrays:“如果单词被双引号括起来,${name[*]} 将扩展为单个单词,其中每个数组成员的值由IFS 变量的第一个字符分隔,...”。
【解决方案9】:

这里有很多关于在 bash 中创建多维数组的答案。

而且无一例外,都是迟钝且难用的。

如果 MD 数组是必需的标准,那么是时候做出决定了:

使用支持 MD 数组的语言

我的首选是 Perl。大多数人可能会选择 Python。 两者都可以。

将数据存储在别处

JSON 和 jq 已经被推荐了。还建议使用 XML,尽管 JSON 和 jq 可能更简单。

Bash 似乎不是您需要做的最佳选择。

有时正确的问题不是“我如何在工具 Y 中做 X?”,而是“哪种工具最适合做 X?”

【讨论】:

    【解决方案10】:

    我从 bash 4 开始使用 associative arrays 执行此操作,并将 IFS 设置为可以手动定义的值。

    这种方法的目的是将数组作为关联数组键的值。

    为了将 IFS 设置回默认值,只需取消设置即可。

    • unset IFS

    这是一个例子:

    #!/bin/bash
    
    set -euo pipefail
    
    # used as value in asscciative array
    test=(
      "x3:x4:x5"
    )
    # associative array
    declare -A wow=(
      ["1"]=$test
      ["2"]=$test
    )
    echo "default IFS"
    for w in ${wow[@]}; do
      echo "  $w"
    done
    
    IFS=:
    echo "IFS=:"
    for w in ${wow[@]}; do
      for t in $w; do
        echo "  $t"
      done
    done
    echo -e "\n or\n"
    for w in ${!wow[@]}
    do
      echo "  $w"
      for t in ${wow[$w]}
      do
        echo "    $t"
      done
    done
    
    unset IFS
    unset w
    unset t
    unset wow
    unset test
    

    下面脚本的输出是:

    default IFS
      x3:x4:x5
      x3:x4:x5
    IFS=:
      x3
      x4
      x5
      x3
      x4
      x5
    
     or
    
      1
        x3
        x4
        x5
      2
        x3
        x4
        x5
    

    【讨论】:

    • 当您将test=("x3:x4:x5") 引用为$test 时,我认为没有理由使用它:您可以简单地使用test="x3:x4:x5"
    • 是的,你完全正确。感谢您的提示。
    【解决方案11】:

    Bash 不支持多维数组,但是我们可以使用 Associate 数组来实现。这里的索引是检索值的关键。关联数组在 bash 版本 4 中可用。

    #!/bin/bash
    
    declare -A arr2d
    rows=3
    columns=2
    
    for ((i=0;i<rows;i++)) do
        for ((j=0;j<columns;j++)) do
            arr2d[$i,$j]=$i
        done
    done
    
    
    for ((i=0;i<rows;i++)) do
        for ((j=0;j<columns;j++)) do
            echo ${arr2d[$i,$j]}
        done
    done
    

    【讨论】:

      【解决方案12】:

      我有一个非常简单但聪明的解决方法: 只需在其名称中定义带有变量的数组。例如:

      for (( i=0 ; i<$(($maxvalue + 1)) ; i++ ))
        do
        for (( j=0 ; j<$(($maxargument + 1)) ; j++ ))
          do
          declare -a array$i[$j]=((Your rule))
        done
      done
      

      不知道这是否有帮助,因为它不完全符合您的要求,但它对我有用。 (同样可以使用没有数组的变量来实现)

      【讨论】:

        【解决方案13】:
        echo "Enter no of terms"
        read count
        for i in $(seq 1 $count)
        do
          t=` expr $i - 1 `
          for j in $(seq $t -1 0)
          do
            echo -n " "
          done
          j=` expr $count + 1 `
          x=` expr $j - $i `
          for k in $(seq 1 $x)
          do
            echo -n "* "
          done
          echo ""
        done
        

        【讨论】:

        • 介意添加一些关于代码应该做什么的解释性文字吗?虽然一些格式也很有帮助,但一些 cmets 会进一步改进答案。虽然可以看到多个数组,但这里看不到多维数组。
        猜你喜欢
        • 2011-09-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-04-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多