数组
1、什么是数组
数组是一种可以一次存放多个值的变量,其组织形式类似于表格。以电子表格为例,电子表格就像一个二维数组,它由行和列组成,根据行与列的地址就可以定位其中的单元格。数组的工作方式也是这样,数组中的单元格叫作元素,每个元素都含有数据。数组元素可以通过索引来访问。
它由行和列组成,根据行与列的地址就可以定位其中的单元格。数组的工作方式也是这样,数组中的单元格叫作元素,每个元素都含有数据。数组元素可以通过索引来访问。
大多数编程语言支持多维数组。电子表格是包含宽度和高度两个维度的多维数组。不少编程语言支持任意维度的数组,不过用得最多的可能是二维和三维数组。
Bash中的数组是一维的,我们可以将其想象为单列的电子表格。即便有此限制,数组仍有不少方面的应用。在Bash 2中首次引入了对数组的支持,最初的UNIX Shell程序sh并不支持数组。
1.1、创建数组
数组变量和其他Bash变量一样,在访问数组变量时会自动创建。例如:
bash
[me@linuxbox ~]$ a[1]=foo
[me@linuxbox ~]$ echo ${a[1]}
foo
在这里,我们演示了数组元素的赋值和访问。在第一个命令中,数组a的第一个元素被赋值foo。第二个命令显示了第一个元素的值,其中的花括号是为了避免Shell试图对数组元素名执行路径名扩展。
也可以使用declare命令创建数组:
bash
[me@linuxbox ~]$ declare -a a
通过-a选项,declare创建了数组a。
1.2、为数组赋值
为数组赋值有两种方法,单个值可以使用下列方法:
bash
name[subscript]=value
其中,name是数组名,subscript是一个大于或等于0的整数(或者算术表达式)。注意,数组的第一个元素的索引是0,而非1。value是赋给该元素的字符串或整数。
多个值可以使用下列方法:
bash
name=(value1 value2 ...)
其中,name是数组名,value是依次赋给数组元素(从元素0开始)的一系列值。例如,如果我们想将一周中各天的缩写赋给数组days,可以这么做:
bash
[me@linuxbox ~]$ days=(Sun Mon Tue Wed Thu Fri Sat)
也可以通过指定各个值的索引,将值赋给特定元素:
bash
[me@linuxbox ~]$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)
1.3、访问数组元素
数组有什么用?就像电子表格程序可以执行很多数据管理任务一样,不少编程任务也可以使用数组来完成。
来看一个简单的数据采集和呈现示例。我们打算编写一个检查指定目录中文件修改时间的脚本Hour。根据这些数据,输出一个表格,显示文件最后一次修改的小时数。该脚本可用于判断什么时候系统最为活跃。脚本hours的执行结果如下:
bash
[me@linuxbox ~]$ hours .
Hour Files Hour Files
---- ----- ---- -----
00 0 12 11
01 1 13 7
02 0 14 1
03 0 15 7
04 1 16 6
05 1 17 5
06 6 18 4
07 3 19 4
08 1 20 1
09 14 21 0
10 2 22 0
11 5 23 0
Total files = 80
在执行这个脚本时,指定当前工作目录为目标目录,最终生成的表格显示了一天的每个小时(0~23)中有多少文件被改动。相关代码如下:
bash
#!/bin/bash
# hours: 对文件的修改时间进行计数
usage () {
echo "usage: ${0##*/} directory" >&2
}
# 检查参数是否是一个目录
if [[ ! -d "$1" ]]; then
usage
exit 1
fi
# 初始化数组
for i in {0..23}; do hours[i]=0; done
# 收集数据
for i in $(stat -c %y "$1"/* | cut -c 12-13); do
j="${i#0}"
((++hours[j]))
((++count))
done
# 显示数据
echo -e "Hour\tFiles\tHour\tFiles"
echo -e "----\t-----\t----\t-----"
for i in {0..11}; do
j=$((i + 12))
printf "%02d\t%d\t%02d\t%d\n"\
"$i"\
"${hours[i]}"\
"$j"\
"${hours[j]}"
done
printf "\nTotal files = %d\n" $count
该脚本由一个函数(usage)和包含4部分的主体代码组成。在第一部分中,我们检查是否指定了命令行参数以及该参数是否为目录。如果不是,则显示用法信息并退出脚本。
第二部分将数组hours的所有元素赋值为0,完成数组的初始化。数组在使用之前并不是必须做预处理,但是我们的脚本需要确保所有元素不为空。注意循环的构建方式。利用花括号扩展{0...23},我们可以轻松地产生for命令所需的单词序列。
第三部分通过对目录中的每个文件执行stat命令来采集数据。我们使用cut从结果中提取占两个数位的小时数。在循环内部,需要删除小时数的前导0,因为Shell会尝试将值00~09解释为八进制数。接下来,增加对应于小时数的数组元素值。最后,增加计数器(count)的值,跟踪目录中的总文件数量。
第四部分显示数组内容。首先输出表头,然后进入循环,生成4列输出。最后,显示总文件数量。
2、数组操作
很多常见的数组操作,例如确定数组元素的数量、删除数组、数组排序等,在脚本编程中有诸多应用。
2.1、输出数组的全部内容
索引*和@可用于访问数组的所有元素。和位置参数一样,但@更实用一些。来看下面的演示:
bash
[me@linuxbox ~]$ animals=("a dog" "a cat" "a fish")
[me@linuxbox ~]$ for i in ${animals[*]}; do echo $i; done
a
dog
a
cat
a
fish
[me@linuxbox ~]$ for i in ${animals[@]}; do echo $i; done
a
dog
a
cat
a
fish
[me@linuxbox ~]$ for i in "${animals[*]}"; do echo $i; done
a dog a cat a fish
[me@linuxbox ~]$ for i in "${animals[@]}"; do echo $i; done
a dog
a cat
a fish
我们创建了数组animals并为其赋值了3个长度为2个单词的字符串,接着执行了4个循环,观察单词分割对数组内容的效果。如果不使用引号, a n i m a l s ∗ 和 {animals\*}和 animals∗和{animals@}的效果一样。*表示法会产生一个包含数组全部内容的单词,而@表示法会产生3个长度为2个单词的字符串,这正是数组"真正的"内容。
2.2、确定数组元素的数量
通过参数扩展,我们可以使用类似获取字符串长度的方式来确定数组中元素的数量。来看一个例子:
bash
[me@linuxbox ~]$ a[100]=foo
[me@linuxbox ~]$ echo ${#a[@]} # number of array elements
1
[me@linuxbox ~]$ echo ${#a[100]} # length of element 100
3
我们创建了数组a,将字符串foo赋值给数组元素100。接着,使用@表示法,通过参数扩展检查数组长度。最后,查看包含字符串foo的数组元素100的长度。注意,在为数组元素100赋值字符串的时候,Bash报告该数组只有一个元素。这和有些编程语言的不一样,后者会将未使用的数组元素(元素0~99)初始化为空并纳入统计范围。在Bash中,数组元素仅在被赋值的时候才存在,无论其索引是什么。
2.3、查找数组使用的索引
在为数组元素赋值时,Bash允许数组中出现"间隙"。有时候我们需要确定哪些数组元素是存在的,这可以通过参数扩展来做到:
bash
${!array[*]}
${!array[@]}
其中,array是数组变量名。和使用*和@等表达式一样,出现在双引号中的@最为实用,因为这种形式会扩展成多个独立的单词:
bash
[me@linuxbox ~]$ foo=([2]=a [4]=b [6]=c)
[me@linuxbox ~]$ for i in "${foo[@]}"; do echo $i; done
a
b
c
[me@linuxbox ~]$ for i in "${!foo[@]}"; do echo $i; done
2
4
6
2.4、向数组尾部添加元素
如果我们需要向数组尾部添加元素,那么即便知道数组元素个数也没什么用,因为*和@返回的值无法告诉我们数组的最大索引是多少。幸运的是,Shell提供了相应的解决方法。通过赋值操作符+=,可以自动将值添加到数组尾部。在下面的例子中,我们先将3个值赋给数组foo,再追加3个:
bash
[me@linuxbox ~]$ foo=(a b c)
[me@linuxbox ~]$ echo ${foo[@]}
a b c
[me@linuxbox ~]$ foo+=(d e f)
[me@linuxbox ~]$ echo ${foo[@]}
a b c d e f
2.5、数组排序
和电子表格一样,经常会需要对数组排序。Shell并没有提供直接的排序方法,不过稍微写点儿代码就可以轻松实现排序:
bash
#!/bin/bash
# array-sort: 对数组排序
a=(f e d c b a)
echo "Original array: ${a[@]}"
a_sorted=($(for i in "${a[@]}"; do echo $i; done | sort))
echo "Sorted array: ${a_sorted[@]}"
执行结果如下:
bash
[me@linuxbox ~]$ array-sort
Original array: f e d c b a
Sorted array: a b c d e f
该脚本利用命令替换技巧,将原始数组(a)的内容复制到另一个数组(a通过更改命令管道设计,这个技巧可用于多种数组操作。
2.6、删除数组
unset命令可以删除数组:
bash
[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ echo ${foo[@]}
a b c d e f
[me@linuxbox ~]$ unset foo
[me@linuxbox ~]$ echo ${foo[@]}
[me@linuxbox ~]$
unset也可用于删除单个数组元素:
bash
[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ echo ${foo[@]}
a b c d e f
[me@linuxbox ~]$ unset 'foo[2]'
[me@linuxbox ~]$ echo ${foo[@]}
a b d e f
在这个例子中,我们删除了数组的第3个元素(索引为2)。记住,数组元素从索引0开始,而不是1!另外还要注意,一定要把数组元素引用起来,以免Shell执行路径名扩展。
有意思的是,对数组赋空值并不会清空其内容。
bash
[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo[@]}
b c d e f
引用数组变量时如果不使用索引,则引用的是索引为0的数组元素。
bash
[me@linuxbox ~]$ foo=(a b c d e f)
[me@linuxbox ~]$ echo ${foo[@]}
a b c d e f
[me@linuxbox ~]$ foo=A
[me@linuxbox ~]$ echo ${foo[@]}
A b c d e f
3、关联数组
Bash 4.0及以上版本支持关联数组。关联数组使用字符串,而非整数作为数组索引。这种能力引入了一些有意思的数据管理方法。例如,我们可以创建一个名为colors的数组,使用颜色名称作为索引:
bash
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
和整数索引数组不同,后者只需通过引用就能得以创建,而关联数组必须使用declare命令的-A选项创建。访问关联数组元素的方法和整数索引数组大同小异。
bash
echo ${colors["blue"]}
在后面,我们会介绍一个充分利用关联数组来生成报告的脚本。
4、总结
如果我们在Bash手册页中搜索单词array,会发现很多搜索结果中有Bash使用数组的身影。尽管其中多数用法都相当晦涩艰深,但在某些特殊情况下,指不定也能派上用场。事实上,数组在Shell编程中远未得到充分利用,主要原因在于传统的UNIX Shell程序(如sh)不支持数组。
这实在是一种遗憾,因为数组在其他编程语言中应用广泛,是解决很多编程问题的"利器"。
数组和循环之间存在一种天然连接,通常都会配合使用。下面这种形式的循环尤为适合计算数组索引:
bash
for ((expr; expr; expr))