Contents

在Linux中二维数组的不同 Bash 实现

1. 简介

数组是项目的集合,每个项目都由唯一的键标识。当项目本身是数组时,我们有一个二维 (2D) 数组

在本教程中,我们将深入研究Linux 下Bash 中的二维数组的概念。更具体地说,我们从不同数组类型的一般结构开始。然后我们讨论 Bash 如何处理这种结构。接下来,我们处理二维阵列的不同模拟。最后,我们总结了一些一般性的警告。

我们使用 GNU Bash 5.0.3 在 Debian 10.10 (Buster) 上测试了本教程中的代码。它是 POSIX 兼容的,应该可以在任何这样的环境中工作。

2. 数组

常规的一维 (1D) 数组只是具有索引的一行或一列数据

[0][1][2][3]
(A B C D)

在这里,每个项目 ( element ) 都在括号中。上面是方括号中的唯一索引(key)。

要访问特定元素,我们使用它的 index 和 base。基数是数组的名称。例如,我们数组的第0 项等于a。 项目可以是任何受支持的数据类型。

2.1. 二维数组

我们可以像这样创建一个二维数组:

   [0][1][2][3]
[0](a)(b)(c)(d)
[1](A)(B)(C)(D)

基本上,原始数组中的第0项现在是aA列——一个数组。我们使用第二个索引访问该数组的元素。例如,项目*[0,1]A*。其他项目的情况类似。

二维数组中的行和列的长度通常是固定常数。

2.2. 混合阵列

使用固定的数组维度,我们可以转换上一节中的二维数组:

[0][1][2][3][4][5][6][7]
(a)(b)(c)(d)(A)(B)(C)(D)

我们将 2 行 4 列的 2D 数组映射到 8 (2*4) 个项目的 1D 列表中。要计算元素的索引,我们使用公式*(COLUMNS*row+col)。它“跳过”了COLUMNS元素行。之后,它获取当前行的 col元素。

重要的是,索引也称为键。在某些编程语言中,键的数据类型可以像项的数据类型一样变化。

几乎所有的编程语言都包含数组数据类型。事实上,在大多数弱类型语言中都有一种声明数组的方法。巴什也不例外。

Bash 中的数组项可以是任何东西……除了数组。那么我们如何在 Bash 中创建二维数组呢?

4. 2D Bash 数组

出于我们的目的,我们将最小化 2D 数组的定义。在本文中,它是我们可以使用两个单独的值一致地索引的任何数组。

由于 Bash 不支持开箱即用的 2D 数组,因此我们必须想出自己的方式来使用它们。让我们探索常见的实现。

4.1. 项目 2D 阵列模拟

在 Bash 中,数组只能通过分隔符与字符串区分开来。将数组作为项目的最简单方法之一是当场将它们从字符串转换:

$ sep=','
$ declare -a alpha=()
$ alpha+=("a${sep}b")
$ alpha+=("c${sep}d")
$ row=0
$ col=1
$ IFS="$sep" read -ra alpharow < <(printf '%s' "${alpha[$row]}")
$ echo "${alpharow[$col]}"
b

最初,我们选择逗号作为列分隔符*$sep*。接下来,定义数组,逐行追加。在这种情况下,第一行是*“a,b”,第二行是“c,d”*。

在结构上,IFS (内部字段分隔符)特殊的 shell 变量开始发挥作用。它的默认值包含空格、制表符和换行符。IFS允许像read 这样的内置函数来分隔数组项。

最后,上面的代码片段使用*$row$col来索引主数组,尽管分两步。首先,整行$row$sep上 被分割成一个新的数组$alpharow*。之后,$alpharow使用列*$col*进行索引,该列返回请求的元素。对于常量数组,这已经足够了。

显然,我们不能直接将分隔符$sep*用作普通字符*。可以通过多种方式将其影响降至最低:

  • 使用稀有或转义字符作为分隔符
  • 加倍/乘以或以其他方式增加每个实际的非分隔分隔符,相应地处理
  • 使用字符组合作为分隔符

接下来,我们将讨论一个不受此特定缺点影响的实现。

4.2. 关联二维阵列模拟

这是 Bash 中关联数组的示例:

$ declare -A alpha=([0,0]=a [0,1]=b [1,0]=c [1,1]=d)
$ echo "${alpha[@]}"
c d b a

注意我们如何给出两个索引的外观。因此,我们理论上可以想象数组看起来像这样:

   [0][1]
[0](/a)(b)
[1](/c)(d)

实际上,这些索引只是字符串键:

$ for k in "${!alpha[@]}"; do
  printf "alpha[%s]=%s\n" "$k" "${alpha[$k]}"
done
alpha[1,0]=c
alpha[1,1]=d
alpha[0,1]=b
alpha[0,0]=a
$ echo "${alpha["1,0"]}"
c

我们可以在循环中生成键或像数字索引一样计算它们

然而,即使是微小的偏差也会导致问题:

$ for (( col=0; col<2; col++ )); do
  printf "alpha["1,%s"]=%s\n" "$col" "${alpha["1,$col"]}"
done
alpha[1,0]=c
alpha[1,1]=d
$ for (( col=0; col<2; col++ )); do
  printf "alpha["1,%s"]=%s\n" "$col" "${alpha["1, $col"]}"
done
alpha[1,0]=
alpha[1,1]=

这两个循环之间的区别非常微妙——逗号和*$col*之间的空格。

在管理一个已经很复杂的结构时,这样的错综复杂并没有帮助。因此,存在一种更简单、更通用的解决方案。

4.3. 混合二维阵列仿真

我们可以将上述结构声明为常规数组:

$ ROWS=2
$ COLUMNS=2
$ declare -a alpha=()
$ alpha+=(a b)
$ alpha+=(c d)
$ echo "${alpha[@]}"
a b c d

到目前为止,我们有一个具有已知精确尺寸(COLUMNSROWS)的普通一维数组。要将其完全“转换”为 2D,索引计算也需要更改:

$ row=0
$ col=1
$ index=$((COLUMNS*$row+$col))
$ echo "${alpha[$index]}"

我们已经探索了上面的公式。与关联数组中的字符串不同,这里的索引仍然是 numeric。这是一个明显的优势,因为数字运算语法不太敏感。空格、前导零和其他此类伪影不会影响计算。

一维二维数组仿真有两个条件可以工作:

  • 数组应该可以映射到一个精确的“矩形”,即COLUMNS*ROWS = ELEMENTS
  • 我们事先知道数组的确切尺寸

克服这些限制需要牺牲一些自由。为此,我们可以结合已经讨论过的两种方法。

4.4. 复杂的二维数组实现

我们可能会放弃两个字符作为分隔符。例如逗号 - 用于行,换行 - 用于列。在这些条件下,整个二维数组最初可以是一个字符串:

$ alpharaw='a,b,c,d
e,f,g
h,i,,k
l,,n,o'

接下来,我们使用readarray 将“array to be”字符串*$alpharaw*中的行分隔到一个新数组中:

$ readarray -t -d $'\n' alpharows < <(printf '%s' "$alpharaw")

最后,我们创建用于读取、写入、删除和插入元素的 Bash 函数。我们将只提供一个读取函数的示例。它使用上面几节中讨论的概念:

$ get_element () {
  alpharaw="$1"
  row="$2"
  col="$3"
  sep="${4:-,}"
  local alpharow alpharows
  IFS=$'\n' readarray -t alpharows < <(printf '%s' "$alpharaw")
  if [[ $row -ge ${#alpharows[@]} ]]; then
    echo "Bad row."
    return
  fi
  IFS="$sep" read -ra alpharow < <(printf '%s' "${alpharows[$row]}")
  if [[ $col -ge ${#alpharow[@]} ]]; then
    echo "Bad column."
    return
  fi
  echo "${alpharow[$col]}"
}
$ get_element "$alpharaw" 3 2
n

虽然实现更复杂,但它的使用更加健壮。问题的可能性要小得多。

5. 注意事项

我们已经在上一节中提到了实现的缺点。为简洁起见,我们列举了主要的

  • 实现复杂度
  • 性能问题
  • 稀疏数组
  • 复杂而脆弱的语法
  • 分隔符转义

当然,这个列表并不完整,因为二维数组是复杂的结构。

在选择实现时,我们必须考虑它对我们的需求的利弊。