前言由于工作需要,最近又开始清理shell脚本。虽然大部分命令都是自己经常用到的,但是写脚本的时候总觉得丑。当我看别人写的剧本时,总是很难读懂。shell脚本毕竟不是一门正经的编程语言,它更像是一种混合不同程序供我们调用的工具。所以很多人在写的时候也会想写到哪里去,基本上就像一个超长的main函数,不忍直视。同时,由于历史的原因,shell的版本很多,功能相同的命令也有很多需要我们选择,导致代码规范难以统一。考虑到以上原因,查阅了一些相关文件,发现很多人都考虑过这些问题,也形成了一些不错的文章,但还是有点散。所以在这里把这些文章稍微整理了一下,作为以后自己写脚本的技术规范。代码风格规范开头有个“蛇棍”。所谓shebang,其实就是以“#!”开头的注释。出现在许多脚本的第一行。表示当我们不指定解释器时,一般可以使用默认的解释器。如下:#!/bin/bas解释器当然有很多。除了bash,我们还可以使用如下命令查看机器支持的解释器:#!/bin/$cat/etc/shells#/etc/shells:validloginshells/bin/sh/bin/dash/bin/bash/bin/rbash/usr/bin/screen当我们直接使用./a.sh执行这个脚本时,如果没有shebang,那么就会默认使用$SHELL指定的解释器,否则使用$SHELL指定的解释器shebang将被使用。不过,上面的写法可能不太适应。一般我们会使用下面的方式来指定:#!/usr/bin/envbash这种方式是我们推荐的方式。代码有注释显然是常识,但这里还是要强调一下,这在shell脚本中尤为重要。因为很多单行的shell命令并不是那么好理解,没有注释,维护起来会特别困难。注释的意义不仅仅是说明用途,而是告诉我们注意事项,就像README一样。具体来说,对于shell脚本,注释一般包括以下几部分:shebang脚本的参数脚本的用途脚本的注释脚本的编写时间、作者、版权等每个函数前的注释一些比较复杂的单-linecommandcomments规范参数非常重要。当我们的脚本需要接受参数时,首先要判断参数是否符合规范,并给予适当的回显,让用户了解参数的使用。至少,至少,我们至少要判断参数个数:if[[$#!=2]];thenecho"Parameterincorrect."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。对于if和for语句,千万不要把then和do关键字单独写在一行上,这样看起来很难看。..命名有标准。所谓命名规范,主要包括以下几点:文件名规范,以.sh结尾,便于识别变量名。命名风格必须一致,不能拼写错误。写shell一般要用小写字母和下划线。写脚本的时候尽量使用UTF-8编码,可以支持中文等一些奇怪的字符。不过,虽然我会写中文,但是在写笔记和打日志的时候还是尽量用英文。毕竟很多机器还是不直接支持中文,打的时候可能会出现乱码。这里还有一点需要注意,就是我们在windows下使用utf-8编码编写shell脚本时,一定要注意这个utf-8是否有BOM。默认情况下,windows通过在文件开头添加三个EFBBBF字节来判断utf-8格式,但是在Linux中,默认是没有BOM的。所以,如果我们是在windows下写脚本,一定要注意把编码改成无BOM的utf-8。一般可以用notepad++等编辑器来改。否则在linux下运行会识别前三个字符,报一些不能识别命令的错误。记得添加权限。虽然很小,但是我个人经常忘记没有执行权限,是不能直接执行的,有点烦人。..日志和echolog的重要性不用多说,可以方便我们回过头来修正错误,这在大型项目中是非常重要的。如果这个脚本是供用户直接在命令行使用的,那么我们在执行的过程中必须能够实时回显执行过程,以便于用户进行控制。有时为了提高用户体验,我们会在echo中加入一些特殊效果,比如颜色、闪烁等,具体可以参考文章ANSI/VT100Controlsequences的介绍。要删除的密码不要在脚本中硬编码密码,不要在脚本中硬编码密码,不要在脚本中硬编码密码。重要的事情说三遍,尤其是当脚本托管在Github这样的平台上时。..Toolongtobranch调用某些程序时,参数可能会很长。这个时候为了保证更好的阅读体验,我们可以使用反斜杠来进行分支:./configure–prefix=/usr–sbin-path=/usr/sbin/nginx–conf-path=/etc/nginx/nginx.conf注意反斜杠前有一个空格。CodingDetailsSpecificationCodeEfficiency在使用命令的时候,一定要了解命令的具体使用方法,尤其是数据处理量大的时候,要时刻考虑命令是否会影响效率。比如下面两条sed命令:./configure--prefix=/usr--sbin-path=/usr/sbin/nginx--conf-path=/etc/nginx/nginx.conf他们的功能是一样的,都是用于获取文件***OK。但是第一个命令读取整个文件,而第二个命令只读取第一行。当文件很大时,仅仅这样一条不同的命令就会造成效率上的巨大差异。当然,这只是一个例子,这个例子的正确用法应该是使用head-n1file命令。..经常使用双引号几乎所有的大佬都推荐在使用“$”获取变量的时候加上双引号。不加双引号很多时候会很麻烦,为什么呢?举个例子:#!/bin/sh#已知当前文件夹下有一个文件a.shvar="*.sh"echo$varecho"$var"他的运行结果如下:为什么a.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中默认变量scope的作用域是全局的,比如下面的脚本:#!/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来获取VAR1的值,这时候怎么办呢?比较的方法如下:echo${!VAR1}确实是可行的,但是看着很不舒服,光是理解也有难度。我们不推荐它。事实上,我们不推荐使用eval命令本身。比较舒服的写法如下:echo${!VAR1}通过加一个!在变量名前,可以实现简单的间接引用。但是需要注意的是,上面的方法只能取值,不能赋值。巧妙使用heredocs所谓heredocs也可以看作是一种多行输入法,即在“<<”之后指定一个标识符,然后我们就可以输入多行内容,直到再次遇到该标识符。使用heredocs,我们可以轻松生成一些模板文件:cat>>/etc/rsyncd.conf<
