乐趣区

关于linux: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)➜  ~ echo $((i++))
0
(MoeLove)➜  ~ echo $i
1

能够看到,执行 ((i++)) 后,输入其实是 0,所以触发了 set -e 的退出条件,脚本便退出了。

第二个

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

第二个和第一个的最次要区别在于变量的赋值上,let i=0 的返回值是 0,所以也就会触发 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】

退出移动版