本文为 Bash Reference Manual第6章:Bash Features 第7节:Arrays 的读书笔记。
完整的笔记目录参见Bash学习笔记总目录。
6.7 Arrays
Bash 提供一维索引数组和关联数组变量。任何变量都可以用作索引数组;declare 内置命令可以显式声明一个数组。数组的大小没有最大限制,成员不需要连续索引或赋值。索引数组通过必须扩展为整数 的算术表达式引用(参见 Shell 算术),且索引从零开始 ;关联数组使用任意字符串。除非另有说明,索引数组的索引必须为非负整数。
shell 会对索引数组的下标执行参数和变量扩展、算术扩展、命令替换以及引号移除。由于这可能导致空字符串,所以下标索引会将这些空字符串视为求值为 0 的表达式。
bash
$ declare -a idxarr
$ idxarr[0]=one
$ declare -p idxarr
declare -a idxarr=([0]="one")
$ var=1
$ idxarr[$var]=two
$ declare -p idxarr
declare -a idxarr=([0]="one" [1]="two")
$ var=
$ idxarr[$var]=three
$ declare -p idxarr
declare -a idxarr=([0]="three" [1]="two")
Shell 对关联数组的下标执行波浪号展开、参数和变量展开、算术展开、命令替换以及引号去除。空字符串不能用作关联数组的键。
如果使用以下语法向任何变量赋值,Bash 会自动创建一个索引数组
bash
name[subscript]=value
下标被视为必须求值为大于或等于零的数字的算术表达式。要显式声明一个索引数组,请使用:
bash
declare -a name
以下语法也被接受;下标subscript会被忽略。(参见 Bash 内置命令)。
bash
declare -a name[subscript]
关联数组使用以下语法创建:
bash
declare -A name
可以使用 declare 和 readonly 内置命令为数组变量指定属性。每个属性都适用于数组的所有成员。
💡 关联数组必须显式的创建,而索引数组不需要。
bash
$ unset idxarray
$ idxarray[1]=jan
$ echo ${idxarray@A}
declare -a idxarray
## 没有显式创建关联数组
$ unset asoarray
$ asoarray=([name]="grace" [gender]="male" [age]=18)
$ echo ${asoarray@A}
declare -a asoarray='18'
## 显式创建关联数组
$ unset asoarray
$ declare -A asoarray
$ asoarray=([name]="grace" [gender]="male" [age]=18)
$ echo ${asoarray@A}
declare -A asoarray
数组通过如下形式的复合赋值进行赋值:
bash
name=(value1 value2 ... )
每个值可以是 [subscript]=string 的形式。索引数组赋值只接受字符串。
列表中的每个值都会进行上述的 shell 扩展(参见 Shell 扩展),但如果值是有效的变量赋值,包括括号和下标,则不会进行大括号扩展和单词分割,就像单独的变量赋值一样。
在为索引数组赋值时,如果提供了可选下标,则赋值到该下标;否则,赋值的元素的下标为该语句上一次赋值的最后下标加一。索引从零开始。
在给关联数组赋值时,复合赋值中的单词可以是赋值语句(此时需要下标),也可以是一列单词,该列单词被解释为交替出现的键和值序列:name=(key1 value1 key2 value2 ...)。这些的处理方式与 name=([key1]=value1 [key2]=value2 ...) 相同。列表中的第一个单词决定剩余单词的解释方式;列表中的所有赋值必须是同一类型。使用键/值对时,键不能为空或缺失;最后缺失的值会被视为空字符串。
这个语法也被 declare 内建命令接受。可以使用上面介绍的 name[subscript]=value 语法给单个数组元素赋值。
在给索引数组赋值时,如果 name 的下标为负数,这个负数会被解释为相对于 name 的最大索引加1的位置,因此负索引从数组末尾向前计算,索引为 -1 表示数组的最后一个元素。
💡 Bash中的索引数组是稀疏数组,即下标可以不连续,只有实际赋值的元素才占用内存。
索引数组示例:
bash
$ declare -a idxarray
$ idxarray=(1 2 3 4)
$ echo ${#idxarray[@]}
4
$ idxarray[6]=7
$ echo ${idxarray[@]}
1 2 3 4 7
$ echo ${idxarray[5]}
$ echo ${#idxarray[@]}
5
$ idxarray[-1]=-7
$ echo ${idxarray[@]}
1 2 3 4 -7
$ idxarray[-2]=-4
$ echo ${idxarray[@]}
1 2 3 4 -4 -7
关联数组示例:
bash
$ unset asoarray
$ declare -A asoarray
# 尽管 asoarray=(k1 v1 k2 v2 k3 v3 k4 v4) 也可以,但不建议
$ asoarray=([k1]=v1 [k2]=v2 [k3]=v3 [k4]=v4)
$ echo ${#asoarray[@]}
4
$ echo ${asoarray[k1]}
v1
$ asoarray[k8]=v8
$ echo ${#asoarray[@]}
5
使用复合赋值语法赋值时,+= 运算符会将值追加到数组变量中;详见上文的 Shell 参数部分。
💡 Bash中,数组元素默认都是字符串类型,但可以在使用时进行算术运算。
示例:
bash
$ unset intarray
$ declare -a intarray=(1 2 3 4)
$ declare -p intarray
declare -a intarray=([0]="1" [1]="2" [2]="3" [3]="4")
## 此时的+=为字符串拼接操作
$ intarray[0]+=100
$ echo ${intarray[0]}
1100
## 利用数学运算符实现数学操作
$ ((intarray[0]+=100))
$ echo ${intarray[0]}
1200
declare -i可以影响操作的行为,尽管仍以字符串形式存储:
bash
$ unset intarray
$ declare -ai intarray=(1 2 3 4)
$ declare -p intarray
declare -ai intarray=([0]="1" [1]="2" [2]="3" [3]="4")
$ intarray[0]+=100
$ echo ${intarray[0]}
101
数组元素使用 ${name[subscript]} 引用。大括号是必需的,以避免与 Shell 的文件名扩展操作符冲突。如果下标是 @ 或 *,该单词会扩展为数组 name 中的所有成员,除非内建命令或单词扩展的描述另有说明。这些下标在单词出现在双引号内时才有区别 。如果单词使用双引号引用,${name[*]} 会扩展为一个单词 ,其值为每个数组成员的值,并用 IFS 变量的第一个字符分隔;而 ${name[@]} 则会将 name 的每个元素扩展为单独的单词 。当没有数组成员时,${name[@]} 不会扩展为任何内容。如果双引号中的扩展出现在一个单词内部,第一个参数的展开会与原始单词展开的开头部分连接,最后一个参数的展开会与原始单词展开的末尾部分连接。这类似于特殊参数 @ 和 * 的展开方式。
💡 在所有情况下,数组元素使用 ${name[subscript]} 引用。在算术运算符中,还可以用name[subscript]的形式引用。
bash
$ arr=(10 20 30)
$ ((ret=arr[0]+arr[1]))
$ echo $ret
30
$ ((ret=${arr[1]}+arr[2]))
$ echo $ret
50
在 6.5 Shell Arithmetic 中描述了对此的支持:
Within an expression, shell variables may also be referenced by name without using the parameter expansion syntax.
${#name[subscript]} 会扩展为 ${name[subscript]} 的长度。如果下标是@或*,则扩展为数组中元素的数量。
bash
$ arr=(tom jerry steven sabrina judy)
$ echo ${arr[@]}
tom jerry steven sabrina judy
$ echo ${arr[*]}
tom jerry steven sabrina judy
$ set -- "${arr[@]}"
$ echo $#
5
$ set -- "${arr[*]}"
$ echo $#
1
## 数组元素个数
$ echo ${#arr[@]}
5
$ echo ${#arr[*]}
5
## 单词的长度
$ echo ${#arr[0]}
3
$ echo ${#arr[1]}
5
下面这个例子可以更好的说明@和*的区别:
bash
arr=("a b" "c" "d e")
for word in prefix"${arr[@]}"suffix; do
echo "WORD:$word"
done
arr=("a b" "c" "d e")
for word in prefix"${arr[*]}"suffix; do
echo "WORD:$word"
done
他们的输出依次如下:
bash
# "@"
WORD:prefixa b
WORD:c
WORD:d esuffix
# "*"
WORD:prefixa b c d esuffix
如果用于引用索引数组元素的下标计算结果为小于零的数字,它会被解释为相对于数组最大索引加1的位置,因此负数索引会从数组末尾向前计数,而索引 -1 指的是最后一个元素。
引用数组变量而不带下标,相当于使用下标 0 进行引用。任何使用有效下标的变量引用都是有效的;如果需要,Bash 会创建一个数组。
如果数组变量的下标已被赋值,则该数组变量被视为已设置。空字符串是一个有效值。
可以获取数组的键(索引)以及对应的值。${!name[@]} 和 ${!name[*]} 会展开为数组变量 name 中分配的索引。在双引号中使用时,其处理方式类似于特殊参数 @ 和 * 在双引号中的展开。
bash
$ declare -A asoarray
$ asoarray=([k1]=v1 [k2]=v2 [k3]=v3 [k4]=v4)
$ echo ${!asoarray[@]}
k4 k1 k2 k3
$ echo ${asoarray[@]}
v4 v1 v2 v3
$ for k in "${!asoarray[@]}"; do echo ${asoarray[$k]}; done
v4
v1
v2
v3
$ idxarray=(1 2 3 4)
$ echo ${!idxarray[@]}
0 1 2 3
$ echo ${idxarray[@]}
1 2 3 4
💡 Bash的关联数组内部通过哈希表实现,并不保证插入键的顺序。在现象上,与数据库堆表的插入类似。
unset 内置命令用于销毁数组。unset name[subscript ] 会取消索引为 subscript 的数组元素。如上所述,对索引数组的负索引也会按同样方式处理。取消数组变量的最后一个元素并不会取消该变量。unset name ,其中 name 是一个数组,会删除整个数组。当 subscript 为 *或 @ 时,unset name[subscript ] 的行为取决于数组类型。如果 name 是关联数组,它会删除键为 * 或 @ 的元素。如果 name 是索引数组,unset 会删除所有元素,但不会删除数组本身。
bash
$ arr=(1 2 3 4)
$ declare -p arr
declare -a arr=([0]="1" [1]="2" [2]="3" [3]="4")
$ unset arr
$ declare -p arr
-bash: declare: arr: not found
$ arr=(1 2 3 4)
$ echo ${arr[@]}
1 2 3 4
$ unset arr[1]
$ declare -p arr
declare -a arr=([0]="1" [2]="3" [3]="4")
$ arr=(1 2 3 4)
$ unset arr[@]
$ declare -a arr
$
在将带下标的变量名作为命令的参数使用时,例如使用 unset,如果不使用上述描述的字词展开语法(例如,unset a[4]),参数将受到 shell 文件名扩展的影响。如果不希望进行路径名扩展,请引用该参数(例如,unset 'a[4]')。
declare、local 和 readonly 内置命令都接受 -a 选项以指定索引数组,接受 -A 选项以指定关联数组。如果同时提供了这两个选项,则以 -A 为准。read 内置命令接受 -a 选项,将从标准输入读取的单词列表赋值给数组,也可以将标准输入中的值读取到各个数组元素中。set 和 declare 内置命令以一种方式显示数组值,使它们可以作为输入重新使用。其他内置命令也接受数组名作为参数(例如 mapfile);有关详细信息,请参阅各个内置命令的描述。Shell 提供了许多内置数组变量。
mapfile或readarray可以标准输入导入索引数组。可参见4.2 Bash Builtin Commands的笔记。