数组

    在本章中,我们将看看另一种数据结构叫做数组,数组能存放多个值。数组几乎是所有编程语言的一个特性。shell 也支持它们,尽管以一个相当有限的形式。即便如此,为解决编程问题,它们是非常有用的。

    数组是一次能存放多个数据的变量。数组的组织结构就像一张表。我们拿电子表格举例。一张电子表格就像是一个二维数组。它既有行也有列,并且电子表格中的一个单元格,可以通过单元格所在的行和列的地址定位它的位置。数组行为也是如此。数组有单元格,被称为元素,而且每个元素会包含数据。使用一个称为索引或下标的地址可以访问一个单独的数组元素。

    大多数编程语言支持多维数组。一个电子表格就是一个多维数组的例子,它有两个维度,宽度和高度。许多语言支持任意维度的数组,虽然二维和三维数组可能是最常用的。

    Bash 中的数组仅限制为单一维度。我们可以把它们看作是只有一列的电子表格。尽管有这种局限,但是有许多应用使用它们。对数组的支持第一次出现在 bash 版本2中。原来的 Unix shell 程序,sh,根本就不支持数组。

    36.2 创建一个数组

    数组变量就像其它 bash 变量一样命名,当被访问的时候,它们会被自动地创建。这里是一个例子:

    这里我们看到一个赋值并访问数组元素的例子。通过第一个命令,把数组 a 的元素1赋值为 “foo”。第二个命令显示存储在元素1中的值。在第二个命令中使用花括号是必需的,以便防止 shell 试图对数组元素名执行路径名展开操作。

    也可以用 declare 命令创建一个数组:

    使用 -a 选项,declare 命令的这个例子创建了数组 a。

    有两种方式可以给数组赋值。单个值赋值使用以下语法:

    1. name[subscript]=value

    这里的 name 是数组的名字,subscript 是一个大于或等于零的整数(或算术表达式)。注意数组第一个元素的下标是0,而不是1。数组元素的值可以是一个字符串或整数。

    多个值赋值使用下面的语法:

    1. name=(value1 value2 ...)

    这里的 name 是数组的名字,value… 是要按照顺序赋给数组的值,从元素0开始。例如,如果我们希望把星期几的英文简写赋值给数组 days,我们可以这样做:

    1. [me@linuxbox ~]$ days=(Sun Mon Tue Wed Thu Fri Sat)

    还可以通过指定下标,把值赋给数组中的特定元素:

    1. [me@linuxbox ~]$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)

    36.4 访问数组元素

    那么数组对什么有好处呢? 就像许多数据管理任务一样,可以用电子表格程序来完成,许多编程任务则可以用数组完成。

    1. [me@linuxbox ~]$ hours .
    2. Hour Files Hour Files
    3. ---- ----- ---- ----
    4. 00 0 12 11
    5. 01 1 13 7
    6. 02 0 14 1
    7. 03 0 15 7
    8. 04 1 16 6
    9. 04 1 17 5
    10. 06 6 18 4
    11. 07 3 19 4
    12. 08 1 20 1
    13. 09 14 21 0
    14. 10 2 22 0
    15. 11 5 23 0
    16. Total files = 80

    当执行该 hours 程序时,指定当前目录作为目标目录。它打印出一张表显示一天(0-23小时)每小时内,有多少文件做了最后修改。程序代码如下所示:

    这个脚本由一个函数(名为 usage),和一个分为四个区块的主体组成。在第一部分,我们检查是否有一个命令行参数,且该参数为目录。如果不是目录,会显示脚本使用信息并退出。

    第二部分初始化一个名为 hours 的数组。给每一个数组元素赋值一个0。虽然没有特殊需要在使用之前准备数组,但是我们的脚本需要确保没有元素是空值。注意这个循环构建方式很有趣。通过使用花括号展开({0..23}),我们能很容易为 for 命令产生一系列的数据(words)。

    接下来的一部分收集数据,对目录中的每一个文件运行 stat 程序。我们使用 cut 命令从结果中抽取两位数字的小时字段。在循环里面,我们需要把小时字段开头的零清除掉,因为 shell 将试图(最终会失败)把从 “00” 到 “09” 的数值解释为八进制(见表35-1)。下一步,我们以小时为数组索引,来增加其对应的数组元素的值。最后,我们增加一个计数器的值(count),记录目录中总共的文件数目。

    脚本的最后一部分显示数组中的内容。我们首先输出两行标题,然后进入一个循环产生两栏输出。最后,输出总共的文件数目。

    有许多常见的数组操作。比方说删除数组,确定数组大小,排序,等等。有许多脚本应用程序。

    36.5.1 输出整个数组的内容

    下标 * 和 @ 可以被用来访问数组中的每一个元素。与位置参数一样,@ 表示法在两者之中更有用处。这里是一个演示:

    1. [me@linuxbox ~]$ animals=("a dog" "a cat" "a fish")
    2. [me@linuxbox ~]$ for i in ${animals[*]}; do echo $i; done
    3. dog
    4. a
    5. cat
    6. a
    7. fish
    8. [me@linuxbox ~]$ for i in ${animals[@]}; do echo $i; done
    9. a
    10. dog
    11. cat
    12. a
    13. fish
    14. [me@linuxbox ~]$ for i in "${animals[*]}"; do echo $i; done
    15. a dog a cat a fish
    16. [me@linuxbox ~]$ for i in "${animals[@]}"; do echo $i; done
    17. a dog
    18. a cat
    19. a fish

    我们创建了数组 animals,并把三个含有两个字的字符串赋值给数组。然后我们执行四个循环看一下对数组内容进行分词的效果。表示法 ${animals[ * ]} 和 ${animals[@]}的行为是一致的直到它们被用引号引起来。

    36.5.2 确定数组元素个数

    使用参数展开,我们能够确定数组元素的个数,与计算字符串长度的方式几乎相同。这里是一个例子:

    1. [me@linuxbox ~]$ a[100]=foo
    2. [me@linuxbox ~]$ echo ${#a[@]} # number of array elements
    3. 1
    4. [me@linuxbox ~]$ echo ${#a[100]} # length of element 100
    5. 3

    我们创建了数组 a,并把字符串 “foo” 赋值给数组元素100。下一步,我们使用参数展开来检查数组的长度,使用 @ 表示法。最后,我们查看了包含字符串 “foo” 的数组元素 100 的长度。有趣的是,尽管我们把字符串赋值给数组元素100,bash 仅仅报告数组中有一个元素。这不同于一些其它语言的行为,这种行为是数组中未使用的元素(元素0-99)会初始化为空值,并把它们计入数组长度。

    36.5.3 查找数组使用的下标

    因为 bash 允许赋值的数组下标包含 “间隔”,有时候确定哪个元素真正存在是很有用的。为做到这一点,可以使用以下形式的参数展开:

    1. ${!array[*]}
    2. ${!array[@]}

    这里的 array 是一个数组变量的名字。和其它使用符号 * 和 @ 的展开一样,用引号引起来的 @ 格式是最有用的,因为它能展开成分离的词。

    1. [me@linuxbox ~]$ foo=([2]=a [4]=b [6]=c)
    2. [me@linuxbox ~]$ for i in "${foo[@]}"; do echo $i; done
    3. a
    4. b
    5. c
    6. [me@linuxbox ~]$ for i in "${!foo[@]}"; do echo $i; done
    7. 2
    8. 4
    9. 6

    36.5.4 在数组末尾添加元素

    如果我们需要在数组末尾附加数据,那么知道数组中元素的个数是没用的,因为通过 * 和 @ 表示法返回的数值不能告诉我们使用的最大数组索引。幸运地是,shell 为我们提供了一种解决方案。通过使用 += 赋值运算符,我们能够自动地把值附加到数组末尾。这里,我们把三个值赋给数组 foo,然后附加另外三个。

    1. [me@linuxbox~]$ foo=(a b c)
    2. [me@linuxbox~]$ echo ${foo[@]}
    3. [me@linuxbox~]$ foo+=(d e f)
    4. [me@linuxbox~]$ echo ${foo[@]}

    36.5.5 数组排序

    就像电子表格,经常有必要对一列数据进行排序。Shell 没有这样做的直接方法,但是通过一点儿代码,并不难实现。

    1. #!/bin/bash
    2. # array-sort : Sort an array
    3. a=(f e d c b a)
    4. echo "Original array: ${a[@]}"
    5. a_sorted=($(for i in "${a[@]}"; do echo $i; done | sort))
    6. echo "Sorted array: ${a_sorted[@]}"

    脚本运行成功,通过使用一个复杂的命令替换把原来的数组(a)中的内容复制到第二个数组(a_sorted)中。通过修改管道线的设计,这个基本技巧可以用来对数组执行各种各样的操作。

    36.5.6 删除数组

    删除一个数组,使用 unset 命令:

    1. [me@linuxbox ~]$ foo=(a b c d e f)
    2. [me@linuxbox ~]$ echo ${foo[@]}
    3. a b c d e f
    4. [me@linuxbox ~]$ unset foo
    5. [me@linuxbox ~]$ echo ${foo[@]}
    6. [me@linuxbox ~]$

    也可以使用 unset 命令删除单个的数组元素:

    1. [me@linuxbox~]$ foo=(a b c d e f)
    2. [me@linuxbox~]$ echo ${foo[@]}
    3. a b c d e f
    4. [me@linuxbox~]$ unset 'foo[2]'
    5. [me@linuxbox~]$ echo ${foo[@]}
    6. a b d e f

    在这个例子中,我们删除了数组中的第三个元素,下标为2。记住,数组下标开始于0,而不是1!也要注意数组元素必须用引号引起来为的是防止 shell 执行路径名展开操作。

    有趣地是,给一个数组赋空值不会清空数组内容:

    1. [me@linuxbox ~]$ foo=(a b c d e f)
    2. [me@linuxbox ~]$ foo=
    3. [me@linuxbox ~]$ echo ${foo[@]}
    4. b c d e f

    任何没有下标的对数组变量的引用都指向数组元素0:

    1. [me@linuxbox~]$ foo=(a b c d e f)
    2. [me@linuxbox~]$ echo ${foo[@]}
    3. a b c d e f
    4. [me@linuxbox~]$ foo=A
    5. [me@linuxbox~]$ echo ${foo[@]}
    6. A b c d e f

    36.5.7 关联数组

    现在最新的 bash 版本支持关联数组了。关联数组使用字符串而不是整数作为数组索引。这种功能给出了一种有趣的新方法来管理数据。例如,我们可以创建一个叫做 “colors” 的数组,并用颜色名字作为索引。

    1. declare -A colors
    2. colors["red"]="#ff0000"
    3. colors["green"]="#00ff00"

    不同于整数索引的数组,仅仅引用它们就能创建数组,关联数组必须用带有 -A 选项的 declare 命令创建。

    访问关联数组元素的方式几乎与整数索引数组相同:

    1. echo ${colors["blue"]}

    在下一章中,我们将看一个脚本,很好地利用关联数组,生产出了一个有意思的报告。

    36.6 本章总结

    如果我们在 bash 手册页中搜索单词 “array”的话,我们能找到许多 bash 在哪里会使用数组变量的实例。其中大部分相当晦涩难懂,但是它们可能在一些特殊场合提供临时的工具。事实上,在 shell 编程中,整套数组规则利用率相当低,很大程度上归咎于传统 Unix shell 程序(比如说 sh)缺乏对数组的支持。这样缺乏人气是不幸的,因为数组广泛应用于其它编程语言,并为解决各种各样的编程问题,提供了一个强大的工具。

    数组和循环有一种天然的姻亲关系,它们经常被一起使用。该 for ((expr; expr; expr)) 形式的循环尤其适合计算数组下标。

    Wikipedia 上面有两篇关于在本章提到的数据结构的文章:

    • )