Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shell 之变量(三) #41

Open
toFrankie opened this issue Feb 25, 2023 · 0 comments
Open

Shell 之变量(三) #41

toFrankie opened this issue Feb 25, 2023 · 0 comments
Labels
2022 2022 年撰写 Linux 与 Linux 相关的文章 Shell 与 Shell、Bash、Zsh、Sh 等相关的文章

Comments

@toFrankie
Copy link
Owner

toFrankie commented Feb 25, 2023

配图源自 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[@]}"

未完待续...

@toFrankie toFrankie added Linux 与 Linux 相关的文章 Shell 与 Shell、Bash、Zsh、Sh 等相关的文章 labels Feb 25, 2023
@toFrankie toFrankie added the 2022 2022 年撰写 label Apr 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2022 2022 年撰写 Linux 与 Linux 相关的文章 Shell 与 Shell、Bash、Zsh、Sh 等相关的文章
Projects
None yet
Development

No branches or pull requests

1 participant