前言由于工作需要,最近又开始清理shell脚本。虽然大部分命令都是自己经常用到的,但是写脚本的时候总觉得丑。当我看别人写的剧本时,总是很难读懂。shell脚本毕竟不是一门正经的编程语言,它更像是一种混合不同程序供我们调用的工具。所以很多人在写的时候也会想写到哪里去,基本上就像一个超长的main函数,不忍直视。同时,由于历史的原因,shell的版本很多,功能相同的命令也有很多需要我们选择,导致代码规范难以统一。考虑到以上原因,查阅了一些相关文件,发现很多人都考虑过这些问题,也形成了一些不错的文章,但还是有点散。所以在这里把这些文章稍微整理了一下,作为以后自己写脚本的技术规范。代码风格规范开头有个“蛇棍”。所谓的shebang其实就是#!开头的注释。出现在许多脚本的第一行。当我们不指定解释器时,它表示默认解释器。一般可能是这样:#!/bin/bash当然解释器有很多。除了bash,我们还可以使用如下命令查看机器支持的解释器:$cat/etc/shells#/etc/shells:validloginshells/bin/sh/bin/dash/bin/bash/bin/rbash/usr/bin/screen当我们直接使用./a.sh执行这个脚本时,如果没有shebang,默认使用$SHELL指定的解释器,否则使用shebang指定的解释器。此方法是我们推荐的使用方法。代码有注释显然是常识,但这里还是要强调一下,这在shell脚本中尤为重要。因为很多单行的shell命令并不是那么好理解,没有注释,维护起来会特别困难。注释的意义不仅仅是说明用途,而是告诉我们注意事项,就像README一样。具体来说,对于shell脚本,注释一般包括以下几个部分:shebang脚本的参数、脚本使用、脚本注意事项、脚本编写时间、作者、版权等。每个函数前的注释一些比较复杂的单行命令注释非常标准化参数很重要。当我们的脚本需要接受参数时,首先要判断参数是否符合规范,并给予适当的回显,让用户了解参数的使用。至少,至少,我们至少要判断参数个数:if[[$#!=2]];然后回显“参数不正确”。exit1fi变量和magicnumber一般情况下,我们会在一开始定义一些重要的环境变量来保证这些变量的存在。source/etc/profileexportPATH=”/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/apps/bin/”这个定义有一个很常见的使用,最典型的应用就是当我们本地安装了很多java版本的时候,我们可能需要指定一个java来使用。然后我们将重新定义脚本开头的JAVA_HOME和PATH变量进行控制。同时,一段好的代码通常不会在代码中硬编码很多“幻数”。如果需要,一般一开始就以变量的形式定义,然后在调用的时候直接调用这个变量,方便以后的修改。缩进是有规则的对于shell脚本来说,缩进是个大问题。因为很多需要缩进的地方(比如if,for语句)都不长,很多人懒得缩进,很多人不习惯使用函数,导致缩进功能弱化。其实正确的缩进非常重要,尤其是在写函数的时候,否则我们在阅读的时候很容易把函数体和直接执行的命令搞混。常见的缩进方式主要有“软制表符”和“硬制表符”。所谓软制表符就是用n个空格进行缩进(n通常为2或4)。所谓hardtab当然是指真正的\t字符,这里最好不要撕,只能说各有利弊。反正我习惯用hardtab。对于if和for语句,我们最好不要把then和do关键字单独写在一行上,这样看起来很难看。..命名有标准。所谓命名规范,基本上包括以下几点:文件名规范,以.sh结尾,便于识别变量名。必须有含义,不能拼错。写脚本的时候尽量使用UTF-8编码,可以支持中文等一些奇怪的字符。不过,虽然我会写中文,但是在写笔记和打日志的时候还是尽量用英文。毕竟很多机器还是不直接支持中文,打的时候可能会出现乱码。这里还有一点需要注意,就是我们在windows下使用utf-8编码编写shell脚本时,一定要注意这个utf-8是否有BOM。默认情况下,windows通过在文件开头添加三个EFBBBF字节来判断utf-8格式,但是在Linux中,默认是没有BOM的。所以,如果我们是在windows下写脚本,一定要注意把编码改成无BOM的utf-8。一般可以用notepad++等编辑器来改。否则在linux下运行会识别前三个字符,报一些不能识别命令的错误。当然,跨平台脚本的另一个常见问题是换行符不同。在windows中默认为\r\n,在unix中默认为\n。不过有两个小工具可以很方便的解决这个问题:dos2unix、unix2dos。记得添加权限。虽然很小,但是我个人经常忘记没有执行权限,是不能直接执行的,有点烦人。..日志和echolog的重要性不用多说,可以方便我们回过头来修正错误,这在大型项目中是非常重要的。如果这个脚本是供用户直接在命令行使用,那我们最好能在执行过程中实时回显执行过程,方便用户控制。有时为了提高用户体验,我们会在echo中加入一些特殊效果,比如颜色、闪烁等,具体可以参考文章ANSI/VT100Controlsequences的介绍。要删除的密码不要在脚本中硬编码密码,不要在脚本中硬编码密码,不要在脚本中硬编码密码。重要的事情说三遍,尤其是当脚本托管在Github这样的平台上时。..Toolongtobranch调用某些程序时,参数可能会很长。这个时候,为了保证更好的阅读体验,我们可以使用反斜杠进行分支:./configure\--prefix=/usr\--sbin-path=/usr/sbin/nginx\--conf-path=/etc/nginx/nginx.conf\注意反斜杠前有一个空格。CodingDetailsSpecificationCodeEfficiency在使用命令的时候,一定要了解命令的具体使用方法,尤其是数据处理量大的时候,要时刻考虑命令是否会影响效率。例如下面两条sed命令:sed-n'1p'filesed-n'1p;1q'file它们的功能相同,都是获取文件的第一行。但是第一个命令读取整个文件,而第二个命令只读取第一行。当文件很大时,仅仅这样一条不同的命令就会造成效率上的巨大差异。当然,这只是一个例子,这个例子的正确用法应该是使用head-n1file命令。..经常使用双引号几乎所有的大佬都推荐在使用“$”获取变量的时候加上双引号。不加双引号很多时候会很麻烦,为什么呢?举个例子:#!/bin/sh#已知当前文件夹下有文件a.shvar="*.sh"echo$varecho"$var"运行结果如下:sh*.sh是为什么会这样?其实可以解释为他执行了如下命令:echo*.shecho"*.sh"很多时候,在使用变量作为参数的时候,一定要注意以上几点,仔细理解区别。以上只是一个很小的例子。在实际应用中,这个细节带来的问题太多了。..熟练使用main函数,我们知道像java、C这样的编译型语言都会有一个函数入口。这种结构使得代码非常可读。我们知道哪些是直接执行的,哪些是函数。但剧本不同。脚本是解释型语言,从第一行直接执行到最后一行。如果命令和函数混合在一起,将很难阅读。使用python的朋友都知道,一个标准的python脚本一般是这样的:#!/usr/bin/envpythondeffunc1():passdeffunc2():passif__name__=='__main__':func1()func2()他实现了我们习惯以一种非常巧妙的方式调用main函数,使代码更具可读性。在shell中,我们有一个类似的技巧:#!/usr/bin/envbashfunc1(){#dosth}func2(){#dosth}main(){func1func2}main"$@"我们可以使用这个写法也实现了类似的main函数,使得脚本更加结构化。考虑作用域shell中默认的变量作用域是全局的,比如下面这个脚本:#!/usr/bin/envbashvar=1func(){var=2}funcecho$var他的输出是2而不是1,这显然是不符合我们的编码习惯,容易造成一些问题。因此,与其直接使用全局变量,不如使用localreadonly之类的命令,其次,我们可以使用declare来声明变量。这些方法比使用全局定义要好。使用函数时必须注意函数返回值。shell中函数的返回值只能是整数。估计函数的返回值通常表示函数的运行状态,所以一般是0或者1,够用了,所以就这样设计了。但是,如果必须传递字符串,也可以使用以下解决方法:func(){echo"2333"}res=$(func)echo"Thisisfrom$res."这样,通过echo或print就可以达到传递一些额外参数的目的。间接参考值什么是间接参考?比如下面这个场景:VAR1="2323232"VAR2="VAR1"我们有一个变量VAR1,还有一个变量VAR2,这个VAR2的值就是VAR1的名字,那么我们现在要通过VAR2,这时候我该怎么办呢?比较的方法是这样的:evalecho\$$VAR2是什么意思?其实就是构造一个字符串echoXXX,这个XXX是XXX”,这个XXX是VAR2的值VAR1,然后用eval强制解析,这样就变相的实现了这个值。这种用法确实可行,但是看起来很不舒服,直观上很难理解,我们不推荐!在变量名前实现了一个简单的间接引用。但是需要注意的是,用上面的方法,我们只能实现取值,不能赋值,如果要实现赋值,还是老老实实的用eval来处理吧withit:VAR1=VAR2eval$VAR1=233echo$VAR2heredocs的巧妙使用所谓heredocs也可以看成是一种多行输入法,就是在“<<”后面指定一个标识符,然后我们就可以输入了多行内容,直到我们再次遇到它为止的标识符。使用heredocs,我们可以轻松生成一些模板文件:cat>>/etc/rsyncd.conf<
