第35章:数组
上一章中,我们学习了 shell 如何处理字符串和数字。迄今为止我们学过的,在计算机科学圈中,都叫做标量变量(scalar variables);即,变量中仅包含了一个值。
本章中,我们要学习另一种数据结构,叫数组(array),它包含了多个值。数组实际上是每种程序语言中都有的功能。shell 也是支持数组的,尽管其方式相当受限。即便如此,它们还是能非常有效地解决一些类型的编程问题。
什么是数组?
数组是在同一时间内保有多于一个值的变量。数组的组织类似一张表格。让我们试着用一个电子表格为例。一个电子表格的行为就像是一个二维数组(two-dimensional-array)。表格有行和列,表格中一个单独的单元格可以根据其行列地址来定位。数组的行为方式与此相同。一个数组有许多单元格,被称为元素(elements),每个元素包含有数据。一个单个的数组元素用被称为索引(index)或下标(subscript)的地址来访问。
大多数程序语言支持多维数组(multidimensional arrays)。一张电子表格是具有两个维度的多维数组示例,宽度和高度。许多语言支持具有任意数量维度的数组,尽管二维和三维数组可能是最常用的。
在 bash
中的数组被限制为单个维度。我们可以把它们想象成为一张电子表格中的单行。即便在这种限制下,也有许多对于数组的应用。bash
首次支持数组是在第二版中。原生的 Unix shell 程序 sh
,是完全不支持数组的。
创建一个数组
数组变量的命名就跟其它 bash
变量一样,当被访问时,就被自动创建了。例如:
这个例子中,我们看到了对一个数组元素的赋值和访问。第一个命令中,我们将数组 a
的第 1
个元素赋值为 foo
。第二个命令显示了已保存的元素 1
中的值。第二个命令中的花括号是必须的,以防止 shell 试图对数组元素作路径名扩展。
一个数组也可以用 declare
命令来创建。
使用 -a
选项,这个 declare
示例就创建了一个数组 a
。
指定数值到一个数组
可以用两种方式指定数值。单个值可以用下列句法来指定:
name[subscript]=value
其中,name
是数组的名称,而 subscript
是一个大于等于零的整数(或者是算术表达式)。记住,一个数组的第一个元素的下标是 0
,不是 1
。value
是要指定到数组元素中的一个字符串或整数。
多个值可以用下列句法来指定:
name=(value1 value2 ...)
其中 name
是数组的名称,而 value
占位符则是从第 0
个元素开始按顺序分配给数组元素的值。例如,我们想要将一个星期的日期名缩写指定到数组 days
中,可以这么做:
还可以对每个值指定下标来对元素赋值。
访问数组元素
那么,数组有什么好处呢?就如许多数据管理任务可以被电子表格程序执行一样,许多程序任务也可以用数组来执行。
来考虑一个简单的数据收集和展示的例子。我们将构建一个脚本,用来检测指定目录中文件的修改时间。从这个数据,脚本将会输出一个表格,来展示文件的最后修改时间,在一天中的哪个小时内。这样的一个脚本可以被用来确定一个系统在何时是最活跃的。该脚本名为 hours
,产生的结果如下:
我们执行 hours
程序,指定当前目录为目标。它就制造出了这份表格,对于一天中的每个小时,有多少文件被最后修改。代码如下:
这个脚本由一个函数(usage
)和一个包含四个章节的主体构成。第一节中,我们检查了命令行上有一个参数,且是一个目录。如果不是,则显示 usage
消息并退出。
第二节中,通过对每个元素赋值为零,初始化了数组 hours
。并没有什么特别的要求需要在使用前准备好数组,但是我们的脚本中要确保没有空元素。注意循环构建的方式很有意思。通过使用花括号扩展({0..23}
),就可以轻松生成供 for
命令使用的 words
序列。
下一节中,通过对目录中每个文件运行 stat
程序来收集数据。我们用 cut
来提取结果中的两位数小时数。在循环内,我们需要把小时字段的前置零去掉,因为 shell 会尝试(并最终失败)将 00
到 09
解释为八进制数字(见表 34-2)。接下来,我们使数组元素的值随一天中的小时数而增长。最后,我们增加一个计数器(count
)的值,以统计目录中文件的总数。
脚本的最后一节显示了数组的内容。首先输出一组标题栏,随后进入一个循环,生成四列输出。最后输出文件的最终统计。
数组操作
有许多常用的数组操作。诸如删除数组、确定数组长度、排序等等之类的事情在脚本编写中有许多应用。
输出整个数组的内容
下标 *
和 @
可以用来访问数组中每个元素。和位置参数一样,@
表示法更有用些。下面是个演示:
我们创建了数组 animals
并分配了三个字符串,每个字符串有两个单词。随后我们执行四个循环来查看对数组内容的分词效果。记号 ${animals[*]}
和 ${animals[@]}
的行为是相同的,除非在它们外面加上双引号。*
表示法的结果是包含数组内容的单个单词,而 @
表示法的结果是三个二词字符串,符合数组「真实」内容。
确定数组元素的个数
使用参数扩展,我们可以确定一个数组中元素的数目,和确定一个字符串长度的方法类似。示例如下:
我们创建了数组 a
并将字符串 foo
赋值给了元素 100
。随后用参数扩展来确定数组的长度,使用了 @
表示法。最后,我们查看了包含字符串 foo
的元素 100
的长度。有趣的是要注意,当我们将字符串赋值到元素 100
时,bash
报告在数组中仅有一个元素。这与一些其它语言的行为有区别,有些语言会将未被使用的数组元素(0
- 99
)初始化为空值并计算在内。在 bash
中,仅仅是那些被赋值的数组元素是存在的,而不论其下标为何。
查找数组使用的下标
因为 bash
数组在下标的分配中允许包含「间隙」,所以有时候判定哪个元素真实存在就很有用了。这可以使用下列形式的参数扩展来完成:
其中 array
是数组变量的名称。和其它扩展一样,包含在引号内的 @
形式是最有用的,因为这会扩展为单个的单词。
在数组的末尾增加元素
如果我要在数组末尾添加元素,知道了一个数组中元素的数目并没有多少帮助,因为用 *
和 @
记号返回的值,没有告诉我们数组中所用下标的最大值。幸运的是,shell 提供了一个解决方案。使用 +=
赋值运算符,我们可以自动将值附加到一个数组的末尾。这里,我们分配三个值到数组 foo
,然后再附加三个值。
对数组排序
和电子表格一样,常常有必要对一列数据排序。shell 没有直接的办法操作,不过稍微编写一点代码,也不难做到。
执行时,脚本生成如下结果:
脚本将原始数组(a
)的内容复制到了第二个数组(a_sorted
),用了一个棘手的命令替换。这个基本技术通过改变管道的设计,可以用来执行许多类型的对数组的操作。
删除一个数组
要删除一个数组,可以使用 unset
命令。
unset
还可以用来删除单个数组元素。
在这个例子中,我们删除了数组中第三个元素,其下标为 2
。记住,数组下标始于零,而非一!还需注意数组元素必须用引号括起来,以防止 shell 执行路径名扩展。
有意思的是,对一个数组赋一个空值,不会清空其内容。
凡引用一个数组变量而不带下标的,都指向该数组的第 0
个元素。
关联数组
4.0 及以上版本的 bash
支持关联数组(associative arrays)。关联数组使用字符串而不是整数作为数组索引。这种能力使得在管理数据时可以使用一些有趣的新方法。举个例子,我们可以创建一个数组 colors
,然后用颜色名称为索引。
不同于仅需在引用时才创建的整数索引数组,关联数组必须由带 -A
选项的 declare
命令创建。关联数组的元素也能用和整数索引数组类似的方法来访问。
下一章中,我们将学习一个使用关联数组的脚本来创建一份有意思的报表。
总结
如果我们在 bash
的手册页中检索 array,可以找到许多 bash
利用的数组变量。其中大多数都很晦涩,不过它们可以为一些特殊的环境提供偶尔的效用。实际上,数组这整个课题在 shell 编程中是相当的低效的,这绝大部分原因在于传统的 Unix shell 程序(如 sh
)没有对数组的任何支持。如此大量的缺乏支持是不幸的,因为数组在别的程序语言中得到广泛的应用,并提供强大的工具以解决各种程序问题。
数组和循环有天然的亲缘关系,经常结合使用。下面这种形式的循环就特别适合用来计算数组的下标:
for ((expr; expr; expr))
扩展阅读
本章中介绍的数据结构,在维基百科上有两篇文章:
Last updated
Was this helpful?