Shell脚本——数组

数组

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))
相关推荐
yuewell_ai1 小时前
具身机器人一多服务器就崩-告别轮询上事件驱动
运维·服务器·机器人
whysoft1 小时前
创意新用法-把老磁带转录成mp3-第2部分
linux·计算机外设
道尔柯南1 小时前
【Linux】进程信号----1
linux·运维·服务器
逐流人1 小时前
华为云服务全景指南:从基础架构到运维实践
运维·华为云·云计算·云服务·ecs
豆芽脚脚1 小时前
华为鲲鹏arm服务器部署DeepSeek-V4-Flash-0731-w8a8
运维·服务器·arm开发
新时代牛马2 小时前
嵌入式设备驱动开发完整篇:从platform/字符设备到probe、DT 与排障
linux
Mortalbreeze2 小时前
深入理解 Linux IO 模型(四):多路复用 —— epoll
linux·运维·服务器·网络·tcp/ip
CedarQR2 小时前
LubanCat-2 (RK3568) Debian 下让 H3C Magic USB 无线网卡(AIC8800)联网全记录
linux·网络·单片机·嵌入式硬件·debian·3568·h3c
lucybean012 小时前
平板曝气器靠谱供应商甄别方法及选型策略解析
运维