Shell 脚本避坑指南(一)

4,214 阅读4分钟

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」。

大家好,我是张晋涛。

提到 Shell 大家想必不会太陌生,我们通常认为 Shell 是我们和系统交互的接口,执行命令返回输出,比如 bash 、zsh 等。偶尔也会有人把 Shell 和 Terminal(终端)混淆,但这和本文关系不大,暂且略过。

作为一名程序员,我们可能天天都会用到 Shell ,偶尔也会把一些命令组织到一起,写个 Shell 脚本之类的,以便提升我们的工作效率。

然而在看似简单的 Shell 脚本中,可能隐藏着很深的坑。这里我先给出两段简单且相似的 Shell 脚本,大家不妨来看看这两段代码的输出是什么:

#!/bin/bash
set -e -u
i=0
while [ $i -lt 6 ]; do
  echo $i
  ((i++))
done

答案是只会输出一个 0 。

#!/bin/bash
set -e -u
let i=0
while [ $i -lt 6 ]; do
  echo $i
  ((i++))
done

答案是没有任何输出,直接退出。

如果你能解释清楚上面两段代码输出结果的话, 那大概你可以跳过这篇文章后续的内容了。

我先来分解下这段代码中涉及到的主要知识点。

变量声明

变量声明有很多种办法, 但是其行为却各有不同。

我们必须先有个基础认识: Bash 没有类型系统,所有变量都是 string 。 基于这个原因,如果是让变量进行算术运算时,不能像在其他的编程语言中那样直接写算术运算符。这会让 bash 解释为对 string 的操作,而不是对数字的操作。

直接声明

(MoeLove)➜  ~ foo=1+1
(MoeLove)➜  ~ echo $foo
1+1

直接声明最简单,但正如前面提到的,直接声明会默认当作 string 进行处理,不能在声明时进行算术运算。

declare 声明

(MoeLove)➜  ~ declare foo=1+1
(MoeLove)➜  ~ echo $foo
1+1

除去直接声明变量外,比较常用的方法是用 declare 来声明变量,但默认情况下,其声明的变量都是按 string 处理的,无法进行正常的算术运算。

declare 整数属性

declare 在声明变量的时候,可以通过 -i 参数增加整数属性,当变量被赋值时,将进行算术运算。

(MoeLove)➜  ~ declare -i bar=1+1
(MoeLove)➜  ~ echo $bar
2

但要注意的是,增加整数属性后,如果将字符串赋值给它,则会出现解析失败的情况,即:将值设置为 0:

(MoeLove)➜  ~ bar=test
(MoeLove)➜  ~ echo $bar
0

let 声明

另一种办法,我们可以通过 let 命令进行变量的声明,这种方式允许在声明时进行算术运算,同时也支持将其他值赋值给此变量。

(MoeLove)➜  ~ let baz=1+1
(MoeLove)➜  ~ echo $baz
2
(MoeLove)➜  ~ baz=moelove.info
(MoeLove)➜  ~ echo $baz
moelove.info

while 循环

while list-1; do list-2; done

Bash 中 while 语法就是这样,在 while 关键字后是一个序列(list),可以是一个或多个表达式/语句,

需要注意的是,当 list-1 返回值为 0 时, list-2 总是会被执行,并且 while 语句最后的返回值是 list-2 最后一次执行的返回值,或者,如果没执行任何语句的话,则返回 0 。

bash 中的算数计算

这部分的内容大家想必常会用到。我来介绍几种常用的方法:

算术扩展

Bash 中的扩展一共有 7 种,算术扩展只是其中之一。具体而言就是通过类似 $((expression)) 这样的形式,来计算表达式的值。例如:

(MoeLove)➜  ~ echo $((3+7))
10
(MoeLove)➜  ~ x=3;y=7
(MoeLove)➜  ~ echo $((x+y))
10

expr 命令

expr 是 coreutils 软件包提供的一个命令,可对表达式进行计算,或者比较大小之类的。

(MoeLove)➜  ~ x=3;y=7
(MoeLove)➜  ~ expr $x + $y
10
# 比较大小
(MoeLove)➜  ~ expr 2 \< 3
1
(MoeLove)➜  ~ expr 2 \< 1
0

bc 命令

按定义来说,bc 其实是一种支持任意精度和可交互执行的计算语言。它比上述提到的 expr 要强大的多,尤其是它还支持浮点数运算。例如:

一般浮点数计算

(MoeLove)➜  ~ echo "scale=2;7/3"|bc
2.33
(MoeLove)➜  ~ echo "7/3"|bc
2

注意: scale 需要手动指定,它表示小数点后的位数。默认情况下 scale 的值为 0 。

内置函数

bc 还有一些内置函数,可以方便我们进行一些快速的计算,比如可以利用 sqrt() 快速的计算平方根。

(MoeLove)➜  ~ echo "scale=2;sqrt(9)" |bc
3.00
(MoeLove)➜  ~ echo "scale=2;sqrt(6)" |bc
2.44

脚本

此外, bc 还支持一种简单的语法,可以支持声明变量,编写循环和判断语句等。例如:我们可以打印20 以内可以被 3 整除的数:

(MoeLove)➜  ~ echo "for(i=1; i<=20; i++) {if (i % 3 == 0) i;}" |bc
3
6
9
12
15
18

bash 的调试

其实 bash shell 中并没有内置调试器。很多情况下,都是采用重复运行加打印来进行调试。但这种方式不够高效。

这里介绍一种比较直观的,也比较方便的用来调试 shell 代码的办法。以下是一段示例 shell 代码。

(MoeLove)➜  ~ cat compare.sh 
#!/bin/bash
read -p "请输入任意数字: " val
real_val=66
if [ "$val" -gt "$real_val" ]
then
   echo "输入值大于等于预设值"
else
   echo "输入值比预设值小"
fi

为其增加执行权限,或者使用 bash 执行:

(MoeLove)➜  ~ bash compare.sh 
请输入任意数字: 33
输入值比预设值小

详细模式

通过增加 -v 选项,即可开启详细模式,用于查看所执行的命令。当然,我们也可以通过在 shebang 上直接增加 -v 选项, 或者增加 set -v 来开启此模式

(MoeLove)➜  ~ bash -v compare.sh
which () {  ( alias;
 eval ${which_declare} ) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot "$@"
}
#!/bin/bash
read -p "请输入任意数字: " val
请输入任意数字: 33
real_val=66
if [ "$val" -gt "$real_val" ]
then
   echo "输入值大于等于预设值"
else
   echo "输入值比预设值小"
fi
输入值比预设值小

使用 xtrace 模式

我们可以通过增加 -x 参数来进入 xtrace 模式,用于调试执行阶段的变量值。

(MoeLove)➜  ~ bash -x compare.sh
+ read -p '请输入任意数字: ' val
请输入任意数字: 33
+ real_val=66
+ '[' 33 -gt 66 ']'
+ echo 输入值比预设值小
输入值比预设值小

识别未定义变量

以下示例中,我故意写错一个字符。执行脚本后,你会发现没有任何报错,但结果并不是我们预期的。这类可能是手误居多,所以我们需要检查是否存在未绑定的变量。

(MoeLove)➜  ~ cat add.sh 
#!/bin/bash
five=5
ten=10
total=$((five+tne))
echo $total
(MoeLove)➜  ~ bash add.sh
5
(MoeLove)➜  ~ bash -u add.sh
add.sh: line 4: tne: unbound variable

增加 -u 选项, 可以检查变量是否未定义/绑定。

组合使用

以上是几种比较常见的使用方式,当然,也可以把它进行组合使用。比如上面的变量未定义的问题, 组合使用 -vu 就可以直接看到具体出现问题的代码是什么内容了。

(MoeLove)➜  ~ bash -vu add.sh
which () {  ( alias;
 eval ${which_declare} ) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot "$@"
}
#!/bin/bash
five=5
ten=10
total=$((five+tne))
add.sh: line 4: tne: unbound variable

将调试信息输出到指定文件

这里我打开了一个特定 FD 上的 debug.log 文件,注意这个 FD 需要与 BASH_XTRACEFD 配置的一致,另外我修改了 PS4 的变量内容,它的默认值是 + 看起来会比较乱,而且没有有效信息,我通过设置 PS4='$LINENO: ' 让它显示行号。

然后在需要调试的位置设置 set -x ,在结束的未知设置 set +x ,这样调试日志中就只会记录我需要调试部分的日志了。

(MoeLove)➜  ~ cat compare.sh 
#!/bin/bash
exec 6> debug.log 
PS4='$LINENO: ' 
BASH_XTRACEFD="6" 
read -p "请输入任意数字: " val
real_val=66
set -x
if [ "$val" -gt "$real_val" ]
then
   echo "输入值大于等于预设值"
else
   echo "输入值比预设值小"
fi
set +x

echo "End"
(MoeLove)➜  ~ bash compare.sh 
请输入任意数字: 88
输入值大于等于预设值
End
(MoeLove)➜  ~ cat debug.log 
8: '[' 88 -gt 66 ']'
10: echo $'\350\276\223\345\205\245\345\200\274\345\244\247\344\272\216\347\255\211\344\272\216\351\242\204\350\256\276\345\200\274'
14: set +x

这里介绍了通过 set 设置选项 的方式较简单,其他的比如使用 trap 加调试的方式也推荐大家去尝试下,这里就不展开了。

回到开始的问题

那我们用刚从介绍的调试方法来执行下开头的两个脚本,并且进行问题的解答。

第一个

(MoeLove)➜  ~ bash -xv demo1.sh
#!/bin/bash
set -e -u
+ set -e -u
i=0
+ i=0
while [ $i -lt 6 ]; do
  echo $i
  ((i++))
done
+ '[' 0 -lt 6 ']'
+ echo 0
0
+ (( i++ ))

从上述调试结果可以看到,这个脚本在输出 0 然后执行完 ((i++)) 后退出。为什么呢? 主要是由于在脚本顶部增加的 set -e 选项。

该选项在遇到首个非 0 值的时候会直接退出。 我们来解释下:

(MoeLove)➜  ~ i=0
(MoeLove)➜  ~ $((i++))
(MoeLove)➜  ~ echo $?
1

可以看到,执行 ((i++)) 后,返回值其实是 1 ,所以触发了 set -e 的退出条件,脚本便退出了。

第二个

(MoeLove)➜  ~ bash -xv demo2.sh
#!/bin/bash
set -e -u
+ set -e -u
let i=0
+ let i=0

第二个和第一个的最主要区别在于变量的赋值上, let i=0 的返回值是 1 ,所以也就会触发 set -e 的退出条件了。我们尝试将第二个脚本修改下,再次执行:

[tao@moelove ~]$ cat demo2-1.sh
#!/bin/bash
set -e -u
let i=1
while [ $i -lt 6 ]; do
  echo $i
  ((i++))
done

[tao@moelove ~]$ bash demo2-1.sh 
1
2
3
4
5

let i=0 修改成 let i=1 即可按预期执行成功。

总结

本篇中,我们主要聊了 bash shell 中的变量声明,循环,数学运算以及 bash shell 的调试。是否对你有所启发呢? 欢迎留言进行交流。

  • 注:本文仅讨论 Bash Shell

欢迎订阅我的文章公众号【MoeLove】 TheMoeLove