Bash 数组

警告
本文最后更新于 2023-10-22,文中内容可能已过时。

Bash 数组

数组(array)是一个包含多个值的变量。成员的编号从 0 开始,数量没有上限,也没有要求成员被连续索引。

数量没有上限和不要求成员连续,真的很自由

数组可以定义为相似类型元素的集合。与大多数编程语言不同,Bash 中的数组不必是相似类型元素的集合。由于 Bash 不能将字符串与数字区分开,因此数组可以同时包含字符串和数字。 Bash 仅支持一维数组(不支持多维数组),并且没有限定数组的长度大小。Bash 还支持关联数组(类似于 Java Map 类型的数据结构)。 Bash 支持下标访问,如果要从最后一个访问数字索引数组,可以使用负索引。索引-1是最后一个元素的参考。可以在数组中使用几个元素

这种方式跟 Python 中的数据容器的下标访问很像,具体请看 Python 的《数据容器.md》的序列的切片操作

输出数组的时候,一个元素换一行

1
$(printf "%s\n" "${hosts_config[@]}")

创建数组

数组可以采用逐个赋值的方法创建。

1
ARRAY[INDEX]=value

上面语法中,ARRAY是数组的名字,可以是任意合法的变量名。INDEX是一个大于或等于零的整数,也可以是算术表达式。注意数组第一个元素的下标是 0,而不是 1。

下面创建一个三个成员的数组。

1
2
3
$ array[0]=val
$ array[1]=val
$ array[2]=val

数组也可以采用一次性赋值的方式创建。

1
2
3
4
5
6
7
8
9
ARRAY=(value1 value2 ... valueN)

# 等同于

ARRAY=(
  value1
  value2
  value3
)

采用上面方式创建数组时,可以按照默认顺序赋值,也可以在每个值前面指定位置。

1
2
3
4
5
$ array=(a b c)
$ array=([2]=c [0]=a [1]=b)

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

只为某些值指定位置,也是可以的。

1
names=(hatter [5]=duchess alice)

上面例子中,hatter是数组的 0 号位置,duchess是 5 号位置,alice是 6 号位置。

这种情况下,可以这样理解,先将未指定位置的元素组成数组,然后将指定位置的元素依次插入指定位置

没有赋值的数组元素的默认值是空字符串。

定义数组的时候,可以使用通配符。

1
$ mp3s=( *.mp3 )

上面例子中,将当前目录的所有 MP3 文件,放进一个数组。

具体请看《Bash 的模式拓展》中的星号拓展小节

我们还可以通过大括号拓展来快速构建列表:

1
2
3
$ arr=({1..10})
$ echo ${arr[@]}
1 2 3 4 5 6 7 8 9 10

具体请看《Bash 的模式拓展》的大括号扩展小节

先用declare -a命令声明一个数组对象,也是可以的。

1
$ declare -a ARRAYNAME

简单实践如下:

1
2
3
4
5
$ declare -a arr
$ arr[0]=11
$ arr[1]=22
$ echo ${arr[@]}
11 22

我们还可以declare -A声明一个关联数组,

1
$ declare -A MAPARRAY

关于 declare 命令,请看《Bash 变量》的declare 命令小节

什么是关联数组?关联数组可以使用任意的字符串、或者整数作为下标来访问数组元素。有点像Map这种数据结构。

简单实践如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ declare -A asso_arr
$ asso_arr['a']=aaa
$ asso_arr['b']=bbb
$ echo ${asso_arr[@]}
aaa bbb
$ echo ${asso_arr['b']}
bbb
$ echo ${asso_arr['a']}
aaa
$ declare -A site=(["google"]="www.google.com" ["runoob"]="www.runoob.com" ["taobao"]="www.taobao.com")
$ echo ${site['google']}
www.google.com
$ echo ${site['runoob']}
www.runoob.com
$ echo ${site[@]}
www.google.com www.runoob.com www.taobao.com

read -a命令则是将用户的命令行输入,存入一个数组。

1
$ read -a dice

上面命令将用户的命令行输入,存入数组dice

具体请看《Bash read 命令》的参数小节

读取数组

读取单个元素

读取数组指定位置的成员,要使用下面的语法。

1
$ echo ${array[i]}     # i 是索引

上面语法里面的大括号是必不可少的,否则 Bash 会输出数组的第一个元素同时索引部分[i]紧跟其后原样输出。

简单实践如下:

1
2
3
4
5
$ arr=(aa bb)
$ echo ${arr[0]}
aa
$ echo $arr[0]
aa[0]

数组的下标支持负数,从最后一位元素(-1)开始遍历。

从左往右数,即正序的时候,第一个字符的下标是 0,从右往左数,即倒序的时候,第一个数的下标是 -1。

读取所有成员

@*是数组的特殊索引,表示返回数组的所有成员。这两个特殊索引配合for循环,就可以用来遍历数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ arr=({1..10})
$ for ele in ${arr[@]};do echo $ele;done
1
2
3
4
5
6
7
8
9
10

注意:@*放不放在双引号之中,是有差别的。如果数组元素自带空格的话,区别就会显示出来。

先看${arr[@]}放在引号中和不放引号中的区别:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ arr=(aaa bbb  "ccc ddd"  eeee "fff ggg")
# 结果不正确
$ for ele in ${arr[@]};do echo $ele;done
aaa
bbb
ccc
ddd
eeee
fff
ggg
# 结果正确
$ for ele in "${arr[@]}";do echo $ele;done
aaa
bbb
ccc ddd
eeee
fff ggg

可以看到,将${arr[@]}放到引号中才能正确遍历,不放到引号中的时候,数组元素的内容会以空拆分成多个元素输出,这是不对的。

再来看看${arr[*]}放到引号中和不妨到引号中的区别

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ arr=(aaa bbb  "ccc ddd"  eeee "fff ggg")
# 结果不正确
$ for ele in ${arr[*]};do echo $ele;done
aaa
bbb
ccc
ddd
eeee
fff
ggg
# 结果不正确,且只有一个元素
$ for ele in "${arr[*]}";do echo element:  $ele;done
element: aaa bbb ccc ddd eeee fff ggg

${arr[*]}不放在双引号之中,跟${arr[@]}不放在双引号之中是一样的。但是当${arr[*]}放在双引号之中,整个数组变成了一个字符串,即只有一个元素用于循环。

因此,我们不建议使用${arr[*]}访问数组的所有元素,推荐使用${arr[@]}

拷贝一个数组的最方便方法,就是写成下面这样。

1
$ arr_copy=( "${arr[@]}" )

这种写法也可以用来为新数组添加成员。

1
$ arr_copy=( "${arr[@]}" aaa "bbb ccc")

简单实践如下:

1
2
3
4
5
6
7
8
9
$ arr_copy=( "${arr[@]}" hhh "iiii  jjjj "  )
$ for ele in "${arr_copy[@]}";do echo $ele;done
aaa
bbb
ccc ddd
eeee
fff ggg
hhh
iiii jjjj

这种复制数组的写法,很像 Python 的自动拆包,具体请看《Python 解包》

默认位置

如果读取数组成员时,没有读取指定哪一个位置的成员,默认使用0号位置。

1
2
3
4
$ declare -a foo
$ foo=A
$ echo ${foo[0]}
A

上面例子中,foo是一个数组,赋值的时候不指定位置,实际上是给foo[0]赋值。

引用一个不带下标的数组变量,则引用的是0号位置的数组元素。

1
2
3
4
5
$ foo=(a b c d e f)
$ echo ${foo}
a
$ echo $foo
a

上面例子中,引用数组元素的时候,没有指定位置,结果返回的是0号位置。

数组的长度

要想知道数组的长度(即一共包含多少成员),可以使用下面两种语法。

1
2
${#array[*]}
${#array[@]}

这个语法跟获取字符串长度是一样的,获取字符串长度${#str}

提取数组序号

数组的元素的数量没有上限,也没有要求成员被连续索引。

${!array[@]}${!array[*]},可以返回数组的成员序号,即哪些位置是有值的。

1
2
3
4
5
$ arr=([5]=a [9]=b [23]=c)
$ echo ${!arr[@]}
5 9 23
$ echo ${!arr[*]}
5 9 23

上面例子中,数组的 5、9、23 号位置有值。

利用这个语法,也可以通过for循环遍历数组。

1
2
3
4
5
arr=(a b c d)

for i in ${!arr[@]};do
  echo ${arr[i]}
done

我们在删除元素之后,实际上并没有删除元素,而只是将其设置为了空字符串,数组的元素并没有变少,数组的长度也没有变,这样其实挺不方便,我们可以通过以下这段脚本,清理数组中的空字符串元素,返回一个不包含空字符串元素的数组。

创建arr_clean.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/bin/env bash

arr=([2]=111 [8]=222 [9]=3333)
echo "${arr[@]}"
echo "${!arr[@]}"
i=0
for index in "${!arr[@]}"; do
    arr_new[i]=${arr[index]}
    ((i++))
done
echo "${arr_new[@]}"
echo "${!arr_new[@]}"
exit

赋权后执行

1
2
3
4
5
$ ./arr_clean.sh 
111 222 3333
2 8 9
111 222 3333
0 1 2

可以看到数组的索引重新整理为连续的。

提取子数组

${array[@]:position:length}的语法可以提取数组成员。

这个语法跟字符串获取子串的语法${varname:offset:length}不一样,不支持负数下标

关于字符串的子串,请看《Bash 字符串操作》的子字符串小节

1
2
3
4
5
$ food=( apples bananas cucumbers dates eggs fajitas grapes )
$ echo ${food[@]:1:1}
bananas
$ echo ${food[@]:1:3}
bananas cucumbers dates

上面例子中,${food[@]:1:1}返回从数组 1 号位置开始的 1 个成员,${food[@]:1:3}返回从 1 号位置开始的 3 个成员。

如果省略长度参数length,则返回从指定位置开始的所有成员。

1
2
$ echo ${food[@]:4}
eggs fajitas grapes

上面例子返回从 4 号位置开始到结束的所有成员。

注意,${array[@]:position:length}返回的是一个列表,或者说序列,其本质上是一个字符串,而不是一个数组,需要将其放到()中,才是一个数组。

简单实践如下:

1
2
3
4
5
6
7
8
9
$ arr=(111 222 333 444 555 666)
$ arr_sub=${arr[@]:2:2}
$ echo ${arr_sub[@]}
333 444
$ echo ${#arr_sub[@]}
1
$ arr_sub=(${arr[@]:2:2})
$ echo ${#arr_sub[@]}
2

此时是一个全新的数组,跟原数组已经没有关系了。所以对这个子数组的修改,不会同步到原数组

Bash 中不存在对象,所以也不存在引用传递

两个数组的合并

两个数组的合并,可以通过arrA+=arrB这种格式,此时 arrB 的元素会追加到 arrA 的后面。

我们可以通过这种语法来在数组末尾追加成员,只要将这个成员通过()包装成一个数组即可。这样就能够实现自动地把值追加到数组末尾的效果。否则,就需要知道数组的最大序号,比较麻烦。

1
2
3
4
5
6
7
8
9
$ arr=(111 222 333 444 555 666)
$ echo ${arr[@]}
111 222 333 444 555 666
$ arr+=(666 777 888)
$ echo ${arr[@]}
111 222 333 444 555 666 666 777 888
$ for index in {1..15};do arr+=($index);done
$ echo ${arr[@]}
111 222 333 444 555 666 666 777 888 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

删除数组成员

删除一个数组成员,使用unset命令。将某个成员设为空值,可以从${arr[@]}的返回值中“隐藏”这个成员。同时${#arr[@]}${!arr[@]}也会相应减少

关于unset命令,请看《Bash 变量》的删除变量

1
2
3
4
5
6
7
8
$ arr=(111 222 333)
$ unset arr[1]
$ echo ${arr[@]}
111 333
$ echo ${#arr[@]}
2
$ echo ${!arr[@]}
0 2

将数组的成员设为空字符串,也可以实现让某个成员从${arr[@]}的返回值中隐藏,注意,这里是“隐藏”,而不是删除,因为这个成员仍然存在,只是值变成了空值。此时${#arr[@]}${!arr[@]}的结果不会有变化

1
2
3
4
5
6
7
8
$ arr=(111 222 333)
$ arr[1]=""
$ echo ${arr[@]}
111 333
$ echo ${#arr[@]}
3
$ echo ${!arr[@]}
0 1 2

由于空值就是空字符串,所以下面这样写也有隐藏效果,但是不建议这种写法。

1
$ arr[1]=

因此如果不是有特别的目的,建议使用unset删除数组成员。

unset ArrayName可以清空整个数组。

1
2
3
$ arr=(111 222 333)
$ unset arr
$ echo ${arr[@]}

在有些情况下,我们想删除数组中的多个指定元素,比如我们想删除arr数组中的所有的ele元素,此时可以使用${arr[@]/ele}这样的语法,而且,ele部分还可以使用?或者*等通配符来进行匹配。

不过注意,通过${arr[@]/ele}返回的结果并不是一个数组而是一个列表(由 IFS 变量分割的多个字符串),因此如果希望返回一个数组,需要用()包起来,即(${arr[@]/ele})

简单实践如下:

1
2
3
4
5
6
7
8
$ arr=(a b c d a b c d)
$ arr_new=(${arr[@]/b})
$ echo ${arr_new[@]}
a c d a c d
$ echo ${!arr_new[@]}
0 1 2 3 4 5
$ echo ${#arr_new[@]}
6

此时如果把@换成*效果也是一样的。

1
2
3
4
5
6
7
8
$ arr=(a b c d a b c d)
$ arr_new=(${arr[*]/b})
$ echo ${arr_new[@]}
a c d a c d
$ echo ${!arr_new[@]}
0 1 2 3 4 5
$ echo ${#arr_new[@]}
6

而将${arr[@]/ele}放到""之中和不放到""之中,也是有差别的

若将${arr[@]/ele}放到""之中,则删除元素之后,数组长度不变。

1
2
3
4
5
6
7
8
$ arr=(a b c d a b c d)
$ arr_new=("${arr[@]/b}")
$ echo ${arr_new[@]}
a c d a c d
$ echo ${!arr_new[@]}
0 1 2 3 4 5 6 7
$ echo ${#arr_new[@]}
8

${arr[*]/ele}放到""之中,则删除元素之后,数组长度变为 1。

1
2
3
4
5
6
7
8
$ arr=(a b c d a b c d)
$ arr_new=("${arr[*]/b}")
$ echo ${arr_new[@]}
a c d a c d
$ echo ${!arr_new[@]}
0
$ echo ${#arr_new[@]}
1

因此,推荐使用${arr[@]/ele},且不放到""之中

其实这个删除语法,跟字符串中删除特定字符串的语法是一样的,只不过没有删除一个和删除多个的区别,在数组中是都删除。

通配符的简单使用:

1
2
3
4
$ arr=(abcd aab ccc eee)
$ arr_new=(${arr[@]/*ab*})
$ echo ${arr_new[@]}
ccc eee

删除语法的另一种使用方式,用于判断元素是否在数组中:

1
if [ "${Hostname[@]/${host}}" != "${Hostname[@]}" ]; then command;fi 
0%