Shell 之数据类型(三)

配图源自 Freepik

上一篇: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 的值为 truefalse 时,才会命中条件。

四、整数

4.1 算术运算

在 Shell 有两种语法可以进行算术运算。

  • (( ... ))
  • $[ ... ] - 此为旧语法。

其中 (( ... )) 内部的空白符会被忽略,因此 ((1+1))(( 1 + 1 )) 是一样的。

(( ... )) 语法不返回值,只要运算结果不为 0,则表示命令执行成功,否则表示命令执行失败。

若要获取运算结果,需在前面加上 $,即 $(( ... )),使其变成算术表达式,返回运算结果。

$ echo $((1 + 1))
2

(( ... )) 支持这些运算操作:加减乘除、取余(%)、指数(**)、自增(++)、自减(--)。

注意点:

  1. (( ... )) 内部可使用圆括号 () 来改变运算顺序,亦可嵌套。
  2. (( ... )) 内部的变量无需添加 $,因此里面的字符串会被认为是变量。
  3. (( ... )) 内部使用了不存在的变量,不会报错。在 Shell 中访问不存在的变量会返回空值,此时 (( ... )) 会将空值当作 0 处理。
  4. 除法运算的结果总是「整数」。比如 $((5 / 2)) 结果为 2,而不是 2.5
  5. (( ... ))$[ ... ] 语法,都只能做「整数」的运算,否则会报错。
  6. (( ... )) 可以执行赋值运算,比如 $((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 at 1. ksh is the only exception that I know (bash just copied ksh).

这样看,起始索引为 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 开始到结尾的数组项。其中 offsetlength 也支持负值,类似字符串截取,这里不再展开。

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[@]}"

未完待续...

你可能感兴趣的:(Shell 之数据类型(三))