上一篇:Shell 之变量
一、前言
Shell 脚本语言是一门弱类型语言。实际上,它并没有数据类型的概念,无论你输入的是字符串还是数字,都是按照字符串类型来存储的。
至于是什么类型,Shell 会根据上下文去确定具体类型。
举个例子:
$ sum=1+2
$ echo $sum
1+2
以上示例,Shell 认为 1+2
是字符串,而不是算术运算之后将结果再赋值给变量 sum
。
如果你要进行算术运算,可以用 let
命令或 expr
命令。
$ let sum=1+2
$ echo $sum
3
根据 let
命令,Shell 确定了你想要的是算术运算,因此就能得到 3
。
如果非要划分的话,可以有:「字符串」、「布尔值」、「整数」和「数组」。
二、字符串
在 Shell 中,最常见的就是字符串类型了。注意几点:
- 当字符串不包含「空白符」,引号是可选的。若原意就是表示一个字符串,而非整数或数组时,建议使用引号。
- 由单引号包裹的字符,都会原样输出。且单引号包裹的内容不允许再出现单引号,转义也不行。
- 由双引号包裹的字符,一些特殊字符(主要有
$
、`
、\
)会进行扩展或转义。- 若要在双引号内输出
$
、`
、\
、"
字符,使用反斜杠\
进行转义即可。
关于引号的用法,推荐看下 Google Shell Style Guide - quoting。
举个例子:
# ✅
str=Frankie
str='Frankie' # 推荐
str="Frankie"
str="Frankie's" # 推荐
str="Frankie's MacBook Pro" # 推荐
str='Frankie"s MacBook Pro' # 推荐
以上示例语法上是允许的, 以下则是错误示例。
# ❌
str='Frankie's MacBook Pro'
2.1 获取字符串长度
语法为 ${#变量名}
,且 {}
是必须的。
$ str='Frankie'
$ echo ${#str}
7
2.2 截取子串
语法为 ${变量名:起始位置:截取长度}
,注意起始位置从 0
开始计算。
- 若省略截取长度,表示截取从起始位置开始到结尾的子串。
- 起始位置可以是负数,但负数前面必须要要有一个空格,以免与设置变量默认值
${foo:-hello}
的语法混淆。- 截取长度可以是负值,表示要排除从字符末尾开始的 N 个字符。
以上操作,不会改变原字符串,类似 JavaScript 的 Array.prototype.substr()
方法。
比如 ${str:6:5}
,在变量 str
中截取第 6
位(包含)开始,长度为 5
的子串。
$ str='Hello Shell!'
$ echo ${str:6:5}
Shell
以上 ${str:6:5}
可以替换为 ${str: -6:-1}
,表示截取变量 str
中倒数第 6
位(包含)开始,到倒数第 1
个之前的子串。
2.3 字符串搜索与替换
Shell 提供了多种搜索、替换的方法。
具体看这一篇:Bash 字符串操作。请注意,替换方法只有贪婪匹配模式。
2.4 大小写转换
利用 tr
(transform)命令,可实现大小写转换。
$ str='Frankie'
$ echo $str | tr 'a-z' 'A-Z'
FRANKIE
$ echo $str | tr 'A-Z' 'a-z'
frankie
三、布尔值
定义布尔值跟字符串一样
truth=true
falsy=false
注意条件判断即可,举个例子:
bool=false
if $bool; then
echo 'Done'
fi
以上示例,只有变量 bool
的值为 false
,才会进入 then
语句输出 Done
。就算是 bool
未定义、或变量被删除了、或者 bool
的值为空字符,都不会进入 then
语句。
因此,布尔值正确的判断方式,应使用 test
命令,或使用 test
的简写语法 [ ]
或 [[ ]]
。比如:
bool=false
if [ $bool = true ]; then
echo 'Done'
fi
if [ $bool = false ]; then
echo 'Error'
fi
以上判断方式,只有当变量 bool
的值为 true
或 false
时,才会命中条件。
四、整数
4.1 算术运算
在 Shell 有两种语法可以进行算术运算。
(( ... ))
。$[ ... ]
- 此为旧语法。
其中 (( ... ))
内部的空白符会被忽略,因此 ((1+1))
和 (( 1 + 1 ))
是一样的。
(( ... ))
语法不返回值,只要运算结果不为0
,则表示命令执行成功,否则表示命令执行失败。
若要获取运算结果,需在前面加上 $
,即 $(( ... ))
,使其变成算术表达式,返回运算结果。
$ echo $((1 + 1))
2
(( ... ))
支持这些运算操作:加减乘除、取余(%
)、指数(**
)、自增(++
)、自减(--
)。
注意点:
(( ... ))
内部可使用圆括号()
来改变运算顺序,亦可嵌套。(( ... ))
内部的变量无需添加$
,因此里面的字符串会被认为是变量。(( ... ))
内部使用了不存在的变量,不会报错。在 Shell 中访问不存在的变量会返回空值,此时(( ... ))
会将空值当作0
处理。- 除法运算的结果总是「整数」。比如
$((5 / 2))
结果为2
,而不是2.5
。(( ... ))
和$[ ... ]
语法,都只能做「整数」的运算,否则会报错。(( ... ))
可以执行赋值运算,比如$((a = 1))
会将变量a
赋值为1
。
4.2 expr 命令
expr
是一个表达式计算工具。支持:
- 加法运算:
+
- 减法运算:
-
- 乘法运算:
\*
- 除法运算:
/
- 取模运算:
%
注意,这里乘法运算 \*
要加 \
转义,否则 Shell 解析特殊符号。还有,非整数参与运算会报错哦!
$ sum=$(expr 1 + 2)
$ echo $sum
3
4.3 let 命令
let
命令用于将算术运算的结果,赋予一个变量。
$ let sum=1+2
$ echo $sum
3
以上示例,使得变量 sum
等于 1+2
的运算结果。注意,sum=1+2
里面不能有空格。
4.4 小数运算
以上 (( ... ))
和 expr
命令均不支持小数运算,如果想进行小数运算,可以借助 bc
计算器或者 awk
命令。
$ echo 'scale=4; 10/3' | bc
3.3333
其中 scale=4
表示保留四位小数。
4.5 逻辑运算
(( ... ))
也提供了逻辑运算:
-
<
小于 -
>
大于 -
<=
小于或相等 -
>=
大于或相等 -
==
相等 -
!=
不相等 -
&&
逻辑与 -
||
逻辑或 -
!
逻辑否 -
expr1 ? expr2 : expr3
三元条件运算符。若表达式expr1
的计算结果为非零值(算术真),则执行表达式
expr2
,否则执行表达式expr3
。
当逻辑表达式为真,返回
1
,否则返回0
。
五、数组
在 Shell 中,可以用数组来存放多个值,数组元素之间通过「空格」隔开。只支持一维数组,不支持多维数组。
在读取数组成员、遍历数组等方面,bash、zsh 之间会有一定的区别。
5.1 数组起始索引
现代高级编程语言中,它们的数组起始索引多数都是 0
。
但在 Shell 编程语言中,不同的 Shell 解析器其数组起始索引(下标)可能是不同的。比如 bash 的起始索引 0
,zsh 的起始索引是 1
。
摘自 StackExchange:
Virtually all shell arrays (
Bourne
,csh
,tcsh
,fish
,rc
,es
,yash
) start at1
.ksh
is the only exception that I know (bash
just copiedksh
).
这样看,起始索引为 1
的 Shell 解析器占多数。对于习惯了从 0
开始的我来说,这一点是有的难以接受的。关于数组起始索引,有兴趣的可看:CITATION NEEDED。
arr[0]=a
arr[1]=b
以上示例,使用 bash 去解析是没问题的。但用 zsh 解析时,就会报错:assignment to invalid subscript range
。因为 zsh 的起始索引是 1
开始的,所以索引 0
是一个不合法的下标。
5.2 创建数组
可使用以下几种方式来创建数组:
# 创建空数组
arr=()
# 创建数组,按顺序赋值
arr=(val1 val2 ... valN)
# 创建数组,逐项添加
arr[0]=val1
arr[1]=val2
arr[2]=val3
# 创建数组,不按顺序赋值
arr=([2]=val3 [0]=val1 [1]=val2)
# 创建稀疏数组
arr=(val1 [2]=val3 val4)
注意几点:
- 没有赋值的数组元素其默认值是空字符串。
- 以上
[2]=val3
形式不允许有空格。 - 元素之间使用空格隔开。
前面提到,不同类型的 Shell 的起始索引可能是不一样的,因此以上采用 [0]
、[1]
、[2]
等方式设置指定项的值,其表示的第几项元素可能是不相同的。
还可以这样
# 可使用通配符,将当前目录的所有 MP3 文件,放入一个数组
$ mp3s=(*.mp3)
# 用 declare 声明一个关联数组,其索引除了支持整数,也支持字符串。
$ declare -a ARRAYNAME
5.3 读取数组长度
前面介绍过,读取字符串长度的语法为 ${#变量名}
,数组也是类似的。
但要借助数组的特殊索引 @
和 *
,将数组扩展成列表,然后再次使用 #
获取数组的长度。语法有以下两种:
${#array[*]}
${#array[@]}
arr1=(a b c)
arr2=('aa 00' 'bb 11' 'cc 22')
echo ${#arr1[*]}
echo ${#arr1[@]}
echo ${#arr2[*]}
echo ${#arr2[@]}
以上结果均输出 3
。
如果是读取数组某项的长度,则使用 ${#数组变量名[下标]}
的形式。比如:
arr[10]=foo
echo ${#arr[10]}
以上输出 3
,它读取的是索引为 10
的元素的值的长度。
5.4 读取数组单个成员
其语法为 ${数组变量[下标]}
,比如:
$ arr=(a b c)
$ echo ${arr[1]}
基于起始索引的问题,${arr[1]}
输出值可能是 a
,也可能是 b
。
注意,里面的 {}
不能省略,否则 $arr[1]
在 bash 里输出位 a[1]
,相当于 $arr
的值与字符串 [1]
连接,因此结果是 a[1]
。
注意以下语法:
$ arr=(a b c)
$ echo $arr
在 zsh 中,可以输出数组所有项,即 a b c
,在 bash 则是输出数组第一项,即 a
。
若想在各种 Shell 环境下统一,最合理的做法是什么呢?使用类似截取字符串子串的语法:
${array[@]:offset:length}
其中 array[@]
表示所有元素,offset
表示偏移量(总是从 0
开始),length
表示截取长度。这种语法在不同 Shell 环境下总能获得一致行为。因此 ${arr[@]:0:1}
总能正确地取到数组的第一项。
因此,需要兼容 bash 和 zsh 时,应使用
${array[@]:offset:length}
语法而不是${array[subscript]}
语法。
5.5 读取数组所有成员
利用数组的特殊索引 @
和 *
,它们返回数组的所有成员。
$ arr=(a b c)
$ echo ${arr[@]}
$ echo ${arr[*]}
以上 ${arr[@]}
和 ${arr[*]}
都输出数组所有成员 a b c
。因此,利用这两个特殊索引,可配合 for
循环来遍历数组。
for item in "${arr[@]}"; do
echo $item
done
5.6 ${arr[@]}
和 ${arr[*]}
细节区别
其差异,主要体现在 for
循环上。
示例一:
arr=('aa 00' 'bb 11' 'cc 22')
echo 'use @, with double quote:'
for item in "${arr[@]}"; do
echo "--> item: $item"
done
echo 'use @, without double quote:'
for item in ${arr[@]}; do
echo "--> item: $item"
done
都是使用了 @
,区别在于 ${arr[@]}
外层是否使用了「双引号」。bash 输出如下:
use @, with double quote:
--> item: aa 00
--> item: bb 11
--> item: cc 22
use @, without double quote:
--> item: aa
--> item: 00
--> item: bb
--> item: 11
--> item: cc
--> item: 22
这里用高级语言来类比:首先,原数组是 ['aa 00', 'bb 11', 'cc 22']
。如果带双引号 "${arr[@]}"
,遍历的是原数组。如果不带双引号 ${arr[@]}
,相当于内部隐式地做了一次「扁平化」操作,使其变成 ['aa', '00', 'bb', '11', 'cc', '22']
形式,最终遍历的是扁平化后的产物。
因此,在遍历数组时,若使用
@
索引,应该要使用双引号,以保持原有数组的结构。
示例二:
arr=('aa 00' 'bb 11' 'cc 22')
echo 'use *, with double quote:'
for item in "${arr[*]}"; do
echo "--> item: $item"
done
echo 'use *, without double quote:'
for item in ${arr[*]}; do
echo "--> item: $item"
done
都是使用了 *
,区别在于 ${arr[*]}
外层是否使用了「双引号」。bash 输出如下:
use *, with double quote:
--> item: aa 00 bb 11 cc 22
use *, without double quote:
--> item: aa
--> item: 00
--> item: bb
--> item: 11
--> item: cc
--> item: 22
从结果上看, "${arr[*]}"
把数组所有项当成了一个整体,遍历时只有一项。而不带双引号时, ${arr[*]}
与 ${arr[@]}
行为一致,都把原数组扁平化了。
对于类似 arr=(a b c)
的数组(即数组每一项不包含空白符), 循环中 "${arr[@]}"
、 ${arr[@]}
、${arr[*]}
行为都是一致的,而 "${arr[*]}"
同样是把原数组所有项当做一个整体了。
提一下,上述示例输出结果均在 bash 下执行。但在 zsh 环境下,这三种 "${arr[@]}"
、 ${arr[@]}
、${arr[*]}
形式,都能「正确」遍历原数组,不会扁平化。
基于以上细微差异,遍历数组的最佳实践是:应使用
@
,且要带上「双引号」。
5.7 截取数组
其实前面已经提到过了,其语法就是:${array[@]:offset:length}
。比如:
$ fruits=(apple banana lemon pear plum orange watermelon)
$ echo "${fruits[@]:3:2}"
pear plum
其中 offset
为偏移量(总是从 0
开始),length
表示截取长度。它不会改变原数组,类似于 JavaScript 的 Array.prototype.slice()
方法。${array[@]:offset:1}
也是获取数组中某项最兼容的写法。
若 length
省略,截取从 offset
开始到结尾的数组项。其中 offset
和 length
也支持负值,类似字符串截取,这里不再展开。
5.8 追加数组成员
数组末尾追加成员,可以使用 +=
赋值操作符,会自动把值最佳到数字末尾。
$ arr1=(a b c)
$ arr1+=(d e)
$ echo "${arr1[@]}"
a b c d e
注意 +=
前后不能有空格,若追加多项元素,则使用空格隔开。
如果知道了数组下标,也可以使用 arr[index]=xxx
形式添加。但注意若 index
位置已有元素,则会产生覆盖效果。
5.9 删除数组成员
清空数组,应使用 unset
语法。比如:
$ arr=(a b c)
$ unset arr
$ echo "${arr[@]}"
对于 arr=''
这种形式,在 zsh 上可以起到清空数组的作用,而 bash 上是对数组第一项赋值为空字符串而已。
如是删除某项,可以这样:
$ arr=(a b c)
$ unset arr[1]
echo "${arr[@]}"
未完待续...