大家好,我是张锦涛。说到Shell,想必大家都不陌生。我们通常认为Shell是我们与系统交互的接口,执行命令返回输出,如bash、zsh等。偶尔会有人将Shell与Terminal(终端)混淆,但这与这篇文章,所以让我们暂时跳过它。作为一名程序员,我们可能每天都会用到Shell,偶尔也会把一些命令整理在一起,写一个Shell脚本之类的,以提高我们的工作效率。然而,看似简单的Shell脚本中,可能隐藏着很深的坑。这里我先给出两个简单相似的shell脚本,大家不妨看看这两个代码的输出是什么:#!/bin/bashset-e-ui=0while[$i-lt6];doecho$i((i++))done的答案只会输出一个0#!/bin/bashset-e-uleti=0while[$i-lt6];doecho$i((i++))done答案是没有输出,直接退出。如果你能清楚地解释上面两段代码的输出,那么你大概可以跳过本文的其余部分。我先把这段代码涉及到的主要知识点分解一下。变量声明声明变量的方法有很多种,但每种方法都有不同的表现。我们首先要有一个基本的认识:Bash没有类型系统,所有的变量都是字符串。因此,如果要对变量进行算术运算,不能像其他编程语言那样直接写算术运算符。这导致bash解释对字符串而不是数字的操作。直接声明(MoeLove)?~foo=1+1(MoeLove)?~echo$foo1+1直接声明是最简单的,但是前面说了,直接声明默认会被当作字符串,声明的时候不能进行运算手术。declarestatement(MoeLove)?~declarefoo=1+1(MoeLove)?~echo$foo1+1除了直接声明变量外,更常见的方法是使用declare来声明变量,但默认情况下,所有声明的变量都是它被作为字符串处理,不能进行正常的算术运算。declareIntegerattributedeclare在声明变量时,可以通过-i参数添加一个整数属性,当变量被赋值时,会进行算术运算。(MoeLove)?~declare-ibar=1+1(MoeLove)?~echo$bar2但是需要注意的是,在添加了整型属性之后,如果给它赋值的是字符串,则会解析失败,即:设置valueto0:(MoeLove)?~bar=test(MoeLove)?~echo$bar0letstatement另一种方式,我们可以通过let命令来声明变量,它允许在声明的时候进行算术运算,同时赋值其他值也支持此变量。(MoeLove)?~letbaz=1+1(MoeLove)?~echo$baz2(MoeLove)?~baz=moelove.info(MoeLove)?~echo$bazmoelove.infowhileloopwhilelist-1;做清单2;doneBash中的while语法是这样的。while关键字之后是一个序列(列表),它可以是一个或多个表达式/语句。需要注意的是,当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))10expr命令expr是coreutils包提供一个命令来计算表达式,或者比较大小等等。(MoeLove)?~x=3;y=7(MoeLove)?~expr$x+$y10#比较大小(MoeLove)?~expr2\<31(MoeLove)?~expr2\<10bc按定义排序In也就是说,bc实际上是一种支持任意精度、可以交互执行的计算语言。它比上面提到的expr要强大得多,尤其是它还支持浮点运算。例如:一般浮点计算(MoeLove)?~echo"scale=2;7/3"|bc2.33(MoeLove)?~echo"7/3"|bc2注意:scale需要手动指定,它表示小数点后的数字。默认情况下,scale的值为0。内置函数bc也有一些内置函数,可以方便我们进行一些快速的计算,比如使用sqrt()来快速计算平方根。(MoeLove)?~echo"scale=2;sqrt(9)"|bc3.00(MoeLove)?~echo"scale=2;sqrt(6)"|bc2.44script另外,bc支持简单的语法,可以支持声明变量,写循环和判断语句等。比如:我们可以打印20以内能被3整除的数:(MoeLove)?~echo"for(i=1;i<=20;i++){if(i%3==0)i;}"|bc369121518bash调试事实上,bashshell中并没有内置调试器。在很多情况下,调试是通过反复运行和打印来进行的。但是这种方式效率不够。下面介绍一种更直观、更方便的调试shell代码的方法。下面是一个示例shell代码。(萌爱)?~catcompare.sh#!/bin/bashread-p"请输入任意数字:"valreal_val=66if["$val"-gt"$real_val"]thenecho"输入的值大于等于为默认值”elseecho“输入值小于预设值”fi增加执行权限,或者使用bash执行:(萌爱)?~bashcompare.sh请输入任意数字:33输入值为小于verbose模式下的默认值通过添加-v选项,可以开启verbose模式,用于查看执行的命令。当然,我们也可以直接在shebang上加上-v选项,或者加上set-v开启这个模式(萌萌哒)?~bash-vcompare.shwhich(){(alias;eval${which_declare})|/usr/bin/which--tty-only--read-alias--read-functions--show-tilde--show-dot"$@"}#!/bin/bashread-p"请输入任意数字:"val请输入任意数字:33real_val=66if["$val"-gt"$real_val"]thenecho"输入值大于等于预设值"elseecho"输入值小于预设值"fi输入值小于预设值使用xtrace模式,我们可以通过添加-x参数进入xtrace模式,用于调试执行阶段的变量值。(萌爱)?~bash-xcompare.sh+read-p'请输入任意数字:'val请输入任意数字:33+real_val=66+'['33-gt66']'+echo输入值大于presetSmallvalue输入一个小于预设值的值Identifyundefinedvariables下面的例子是我故意打错的。执行脚本后,你会发现并没有报错,但是结果并不是我们所期望的。这种类型可能多半是错误的,所以我们需要检查是否有未绑定的变量。(MoeLove)?~catadd.sh#!/bin/bashfive=5ten=10total=$((five+tne))echo$total(MoeLove)?~bashadd.sh5(MoeLove)?~bash-u添加。shadd.sh:line4:tne:unboundvariable添加-u选项,可以查看变量是否未定义/绑定。组合使用以上是几种常见的使用方式,当然也可以组合使用。比如上面的undefinedvariables的问题,结合-vu可以直接看到有问题的具体代码内容。(MoeLove)?~bash-vuadd.shwhich(){(别名;eval${which_declare})|/usr/bin/which--tty-only--read-alias--read-functions--show-tilde--show-dot"$@"}#!/bin/bashfive=5ten=10total=$((five+tne))add.sh:line4:tne:unboundvariable将调试信息输出到指定文件这里我在特定的FD上创建了一个debug.log文件。注意这个FD需要和BASH_XTRACEFD配置保持一致。另外,我修改了PS4的变量内容。它的默认值是+,看起来很乱,没有有效信息。我设置PS4='$LINENO:'以使其显示行号。然后在需要调试的位置设置-x,在unknown结尾设置+x,这样调试日志中只会记录我需要调试的部分日志。(MoeLove)?~catcompare.sh#!/bin/bashexec6>debug.logPS4='$LINENO:'BASH_XTRACEFD="6"read-p"Enteranynumber:"valreal_val=66set-xif["$val"-gt"$real_val"]thenecho"输入值大于等于默认值"elseecho"输入值小于默认值"fiset+xecho"End"(萌爱)?~bash比较.shPleaseenteranynumber:88输入值大于等于预设值End(MoeLove)?~catdebug.log8:'['88-gt66']'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设置options的方法比较简单。其他的如使用trap加调试等方法也推荐大家尝试,这里不再展开。回到原题,我们用刚才介绍的调试方法,执行前两个脚本,回答问题。首先(MoeLove)?~bash-xvdemo1.sh#!/bin/bashset-e-u+set-e-ui=0+i=0while[$i-lt6];doecho$i((i++))done+'['0-lt6']'+echo00+((i++))从上面的调试结果可以看出,这个脚本在输出0执行完((i++)).为什么?主要是由于在脚本顶部添加了set-e选项。此选项仅在遇到第一个非零值时退出。解释一下:(MoeLove)?~i=0(MoeLove)?~$((i++))(MoeLove)?~echo$?1可以看到执行完((i++)),返回值其实是1,所以set-e的退出条件被触发,脚本退出。第二个(MoeLove)?~bash-xvdemo2.sh#!/bin/bashset-e-u+set-e-uleti=0+leti=0第二个和第一个的主要区别是变量上的赋值时,leti=0的返回值为1,所以也会触发set-e的退出条件。让我们尝试修改第二个脚本并再次执行它:[tao@moelove~]$catdemo2-1.sh#!/bin/bashset-e-uleti=1while[$i-lt6];doecho$i((i++))done[tao@moelove~]$bashdemo2-1.sh12345把leti=0改成leti=1就可以正常执行了。总结在这篇文章中,我们主要讲了bashshell中的变量声明、循环、数学运算和bashshell调试。它对你有启发吗?欢迎留言交流。注意:本文仅讨论BashShell。欢迎订阅我的文章公众号【MoeLove】
