Bash 数组和 Shell Expansion

TL;DR

本文主要讨论了 bash 数组和利用 Shell Expansion 语法对数组进行操作,由于写了一个很蛋疼的 bash 工具故想把 bash 坑爹的数组语法吐槽一下。

bash 的语法确实反人类,如果有选择余地,尽量不要用 bash 实现超过10行的功能,即使用 Python 写一万个 subprocess 也好过和 bash 的奇妙语法斗智斗勇。

bash 数组基本语法

虽然 bash 没有真正的变量和引用,但是他真的有数组(也许很多人并不知道 bash 其实是有数组的?)

本质上 bash 中的变量都可以认为是字符串而不存在数值类型变量,数值计算实际上也是通过一种展开 Arithmetic Expansion 实现,“变量引用”概念实际上是一层 getter

可以显示的声明一个变量为数组类型(不声明也可以直接按数组类型赋值): declare -a array

数组赋值 array=(1 2 3 4)

bash 数组支持稀疏赋值,即只给指定序号的元素赋值: array=([0]=1 [3]=4),此时对数组进行遍历 for i in $(seq 0 4);do echo $i ${array[$i]};done 会看到中间一部分输出只有序号没有内容

注意调试 shell 脚本的一些高级功能的时候,请不要在 zsh 或者 dash(Debian 默认的 /bin/sh 指向 dash),zsh 中数组下标从1开始,而 bash 从0开始,不注意的话容易被坑。

由于 bash 的循环控制并不要求对象是一个数组,也就是 bash 没有 Python 等语言类似 iterable 的属性,一般 bash 写循环的时候是直接 for 一个字符串的(如 for i in $(seq 3)或者直接 for i in 1 2 3),因此 bash 中字符串和数组的语法容易被混淆。

在使用上,数组和字符串在使用上最明显的一个区别是, echo $array 将会只输出数组的第一个元素(有点像 C 数组特性),而 echo 一个普通字符串会打印出全部内容,因此也不能直接使用 echo 或者 printf 来输出一个完整的数组,打印完整数组的正确姿势需要先了解一下参数展开 Parameter Expansion。

Shell Expansion

bash 语法最玄妙的一部分莫过于 Shell expansion,bash 的数组操作很大一部分需要依赖这个特性来实现,其中最复杂的一部分是参数展开(先前提到过,bash 的数值计算完全依赖算术展开 Arithmetic Expansion,这也是 Shell Expansion 的一部分)

参数(Parameter)指的是 Shell Parameter,包括 Positional Parameters 和 Special Parameters

详细的语法参见:https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html

表达式 含义
${var} 变量var的值, 与 $var相同
${var-DEFAULT} 如果var没有被声明, 那么就以 $DEFAULT作为其值
${var:-DEFAULT} 如果var没有被声明, 或者其值为空, 那么就以 $DEFAULT作为其值
${var=DEFAULT} 如果var没有被声明, 那么就以 $DEFAULT作为其值
${var:=DEFAULT} 如果var没有被声明, 或者其值为空, 那么就以 $DEFAULT作为其值
${var+OTHER} 如果var声明了, 那么其值就是 $OTHER, 否则就为null字符串
${var:+OTHER} 如果var被设置了, 那么其值就是 $OTHER, 否则就为null字符串
${var?ERR_MSG} 如果var没被声明, 那么就打印 $ERR_MSG
${var:?ERR_MSG} 如果var没被设置, 那么就打印 $ERR_MSG
${!varprefix } 匹配之前所有以varprefix开头进行声明的变量
${!varprefix@} 匹配之前所有以varprefix开头进行声明的变量
${#string} $string的长度
${string:position} $string中, 从位置 $position开始提取子串
${string:position:length} $string中, 从位置 $position开始提取长度为 $length的子串
${string#substring} 从变量 $string的开头, 删除最短匹配 $substring的子串
${string##substring} 从变量 $string的开头, 删除最长匹配 $substring的子串
${string%substring} 从变量 $string的结尾, 删除最短匹配 $substring的子串
${string%%substring} 从变量 $string的结尾, 删除最长匹配 $substring的子串
${string/substring/replacement} 使用 $replacement, 来代替第一个匹配的 $substring
${string//substring/replacement} 使用 $replacement, 代替所有匹配的 $substring
${string/#substring/replacement} 如果 $string的前缀匹配 $substring, 那么就用 $replacement来代替匹配到的 $substring
${string/%substring/replacement} 如果 $string的后缀匹配 $substring, 那么就用 $replacement来代替匹配到的 $substring

参数展开在 bash 数组中的应用

数组取下标

${!array[@]}

${array:offset:length} 可以起到类似数组切片的效果

数组遍历

这种写法的优点是 zsh 和 bash 通用,无论下标从0还是从1开始都是正常输出的,也可以使用 for i in $(seq 0 $(($length-1)))作为循环条件,虽然看起来比较丑而且需要区分数组起止序号,对于 zsh 应为 for i in $(seq $length)

数组新增

借助 Arithmetic Expansion,bash 数组的新增元素非常简洁: array+=(element)

数组删除

可以借助参数展开的 substring 删除功能进行数组删除,比如 ${array#substring},但是这样其实是一个很坑爹的用法,因为必须指定完整的元素,否则会得到截短的元素,比如一个内容为 array=(aaa bbb ccc)的数组, ${array[@]#aa}的值将是 (a bbb ccc)

也可以借助数组切片的方式间接删除指定序号的元素,依旧以 array=(aaa bbb ccc)为例, ${array[@]:0:1}的值为 aaa, ${array[@]:1:1} 的值为 bbb, ${array[@]:1:2}的值为 bbb ccc,注意是字符串不是一个数组。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.