1

Shell 脚本避坑指南(一)

 2 years ago
source link: https://zhuanlan.zhihu.com/p/436102748
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Shell 脚本避坑指南(一)

大家好,我是张晋涛。

提到 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】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK