作者:Myths
链接:https://blog.mythsman.com/201...
前言
因为工作须要,最近从新开始收拾shell脚本。尽管绝大部分命令本人平时也常常应用,然而在写成脚本的时候总感觉写的很难看。而且当我在看其他人写的脚本的时候,总感觉难以浏览。毕竟shell脚本这个货色不算是正经的编程语言,他更像是一个工具,用来杂糅不同的程序供咱们调用。
因而很多人在写的时候也是想到哪里写到哪里,基本上都像是一段超长的main函数,不忍直视。同时,因为历史起因,shell有很多不同的版本,而且也有很多有雷同性能的命令须要咱们进行取舍,以至于代码的标准很难对立。
思考到下面的这些起因,我查阅了一些相干的文档,发现这些问题其实很多人都思考过,而且 也造成了一些不错的文章,然而还是有点零散。因而我就在这里把这些文章略微整顿了一下,作为当前我本人写脚本的技术规范。
代码格调标准
结尾有“蛇棒”
所谓shebang其实就是在很多脚本的第一行呈现的以#!结尾的正文,他指明了当咱们没有指定解释器的时候默认的解释器,个别可能是上面这样:
#!/bin/bash
当然,解释器有很多种,除了bash之外,咱们能够用上面的命令查看本机反对的解释器:
$ cat /etc/shells#/etc/shells: valid login shells/bin/sh/bin/dash/bin/bash/bin/rbash/usr/bin/screen
当咱们间接应用./a.sh来执行这个脚本的时候,如果没有shebang,那么它就会默认用$SHELL指定的解释器,否则就会用shebang指定的解释器。
这种形式是咱们举荐的应用形式。
代码有正文
正文,显然是一个常识,不过这里还是要再强调一下,这个在shell脚本里尤为重要。因为很多单行的shell命令不是那么浅显易懂,没有正文的话在保护起来会让人尤其的头大。
正文的意义不仅在于解释用处,而在于通知咱们注意事项,就像是一个README。
具体的来说,对于shell脚本,正文个别包含上面几个局部:
- shebang
- 脚本的参数
- 脚本的用处
- 脚本的注意事项
- 脚本的写作工夫,作者,版权等
- 各个函数前的阐明正文
- 一些较简单的单行命令正文
参数要标准
这一点很重要,当咱们的脚本须要承受参数的时候,咱们肯定要先判断参数是否合乎标准,并给出适合的回显,不便使用者理解参数的应用。
起码,起码,咱们至多得判断下参数的个数吧:
if [[ $# != 2 ]];then echo "Parameter incorrect." exit 1fi
变量和魔数
个别状况下咱们会将一些重要的环境变量定义在结尾,确保这些变量的存在。
source /etc/profileexport PATH=”/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/apps/bin/”
这种定义形式有一个很常见的用处,最典型的利用就是,当咱们本地装置了很多java版本时,咱们可能须要指定一个java来用。那么这时咱们就会在脚本结尾从新定义JAVA_HOME以及PATH变量来进行管制。同时,一段好的代码通常是不会有很多硬编码在代码里的“魔数”的。如果肯定要有,通常是用一个变量的模式定义在结尾,而后调用的时候间接调用这个变量,这样不便日后的批改。
缩进有规矩
对于shell脚本,缩进是个大问题。因为很多须要缩进的中央(比方if,for语句)都不长,所有很多人都懒得去缩进,而且很多人不习惯用函数,导致缩进性能被弱化。
其实正确的缩进是很重要的,尤其是在写函数的时候,否则咱们在浏览的时候很容易把函数体跟间接执行的命令搞混。
常见的缩进办法次要有”soft tab”和”hard tab”两种。
- 所谓soft tab就是应用n个空格进行缩进(n通常是2或4)
- 所谓hard tab当然就是指实在的\t字符
- 这里不去撕哪种形式最好,只能说各有各的优劣。反正我习惯用hard tab。
- 对于if和for语句之类的,咱们最好不要把then,do这些关键字独自写一行,这样看上去比拟丑。。。
命名有规范
所谓命名标准,根本蕴含上面这几点:
- 文件名标准,以.sh结尾,不便辨认
- 变量名字要有含意,不要拼错
- 对立命名格调,写shell个别用小写字母加下划线
编码要对立
在写脚本的时候尽量应用UTF-8编码,可能反对中文等一些奇奇怪怪的字符。不过尽管能写中文,然而在写正文以及打log的时候还是尽量英文,毕竟很多机器还是没有间接反对中文的,打进去可能会有乱码。这里还尤其须要留神一点,就是当咱们是在windows下用utf-8编码来写shell脚本的时候,肯定要留神这个utf-8是否是有BOM的。默认状况下windows判断utf-8格局是通过在文件结尾加上三个EF BB BF字节来判断的,然而在Linux中默认是无BOM的。因而如果咱们是在windows下写脚本的时候,肯定要留神将编码改成Utf-8无BOM,个别用notepad++之类的编辑器都能改。否则,在Linux下运行的时候就会辨认到结尾的三个字符,从而报一些无奈辨认命令的错。当然,对于跨平台写脚本还有一个比拟常见的问题就是换行符不同。windows默认是\r\n而unix下是\n。不过有两个小工具能够十分不便的解决这个问题:dos2unix,unix2dos。
权限记得加
这一点尽管很小,然而我集体却常常遗记,不加执行权限会导致无奈间接执行,有点厌恶。。。
日志和回显
日志的重要性不用多说,可能不便咱们回头纠错,在大型的我的项目里是十分重要的。
如果这个脚本是供用户间接在命令行应用的,那么咱们最好还要可能在执行时实时回显执行过程,不便用户掌控。
有时候为了进步用户体验,咱们会在回显中增加一些特效,比方色彩啊,闪动啊之类的,具体能够参考ANSI/VT100 Control sequences这篇文章的介绍。
明码要移除
不要把明码硬编码在脚本里,不要把明码硬编码在脚本里,不要把明码硬编码在脚本里。
重要的事件说三遍,尤其是当脚本托管在相似Github这类平台中时。。。
太长要分行
在调用某些程序的时候,参数可能会很长,这时候为了保障较好的浏览体验,咱们能够用反斜杠来分行:
./configure \–prefix=/usr \–sbin-path=/usr/sbin/nginx \–conf-path=/etc/nginx/nginx.conf \
留神在反斜杠前有个空格。
编码细节标准
代码有效率
在应用命令的时候要理解命令的具体做法,尤其当数据处理量大的时候,要时刻思考该命令是否会影响效率。
比方上面的两个sed命令:
sed -n '1p' filesed -n '1p;1q' file
他们的作用一样,都是获取文件的第一行。然而第一条命令会读取整个文件,而第二条命令只读取第一行。当文件很大的时候,仅仅是这样一条命令不一样就会造成微小的效率差别。
当然,这里只是为了举一个例子,这个例子真正正确的用法应该是应用head -n1 file命令。。。
勤用双引号
简直所有的大佬都举荐在应用”$”来获取变量的时候最好加上双引号。
不加上双引号在很多状况下都会造成很大的麻烦,为什么呢?举一个例子:
#!/bin/sh#已知以后文件夹有一个a.sh的文件var="*.sh"echo $varecho "$var"
他的运行后果如下:
a.sh*.sh
为啥会这样呢?其实能够解释为他执行了上面的命令:
echo *.shecho "*.sh"
在很多状况下,在将变量作为参数的时候,肯定要留神下面这一点,认真领会其中的差别。下面只是一个十分小的例子,理论利用的时候因为这个细节导致的问题切实是太多了。。。
巧用main函数
咱们晓得,像java,C这样的编译型语言都会有一个函数入口,这种构造使得代码可读性很强,咱们晓得哪些间接执行,那些是函数。然而脚本不一样,脚本属于解释性语言,从第一行间接执行到最初一行,如果在这当中命令与函数糅杂在一起,那就十分难读了。
用python的敌人都晓得,一个合乎规范的python脚本大体上至多是这样的:
#!/usr/bin/env pythondef func1(): pass def func2(): pass if __name__=='__main__': func1() func2()
他用一个很奇妙的办法实现了咱们习惯的main函数,使得代码可读性更强。
在shell中,咱们也有相似的小技巧:
#!/usr/bin/env bash func1(){ #do sth } func2(){ #do sth } main(){ func1 func2 } main "$@"
咱们能够采纳这种写法,同样实现相似的main函数,使得脚本的结构化水平更好。
思考作用域
shell中默认的变量作用域都是全局的,比方上面的脚本:
#!/usr/bin/env bash var=1 func(){ var=2 } func echo $var
他的输入后果就是2而不是1,这样显然不合乎咱们的编码习惯,很容易造成一些问题。
因而,相比间接应用全局变量,咱们最好应用local readonly这类的命令,其次咱们能够应用declare来申明变量。这些形式都比应用全局形式定义要好。
函数返回值
在应用函数的时候肯定要留神,shell中函数的返回值只能是整数,预计是因为个别状况下一个函数的返回值通常示意这个函数的运行状态,所以个别都是0或者是1就够了,因而就设计成了这样。不过,如果非得想传递字符串,也能够通过上面变通的办法:
func(){ echo "2333" } res=$(func) echo "This is from $res."
这样,通过echo或者print之类的就能够做到传一些额定参数的目标。
间接援用值
什么叫间接援用?比方上面这个场景:
VAR1="2323232"VAR2="VAR1"
咱们有一个变量VAR1,又有一个变量VAR2,这个VAR2的值是VAR1的名字,那么咱们当初想通过VAR2来获取VAR1的值,这时候应该怎么办呢?
比拟土鳖的办法是这样:
eval echo \$$VAR2
啥意思呢?其实就是结构了一个字符串echo XXX,这个XXX就是XXX”,这个XXX就是VAR2的值VAR1,而后再用eval强制解析,这样就做到了变相取值。
这个用法确实可行,然而看起来非常的不难受,很难直观的去了解,咱们并不举荐。而且事实上咱们自身就不举荐应用eval这个命令。
比拟难受的写法是上面这样:
echo ${!VAR1}
通过在变量名前加一个!就能够做到简略的间接援用了。
不过须要留神的是,用下面的办法,咱们只可能做到取值,而不能做到赋值。如果想要做到赋值,还要老老实实的用eval来解决:
VAR1=VAR2eval $VAR1=233echo $VAR2
巧用heredocs
所谓heredocs,也能够算是一种多行输出的办法,即在”<<”后定一个标识符,接着咱们能够输出多行内容,直到再次遇到标识符为止。
应用heredocs,咱们能够十分不便的生成一些模板文件:
cat>>/etc/rsyncd.conf << EOF log file = /usr/local/logs/rsyncd.log transfer logging = yes log format = %t %a %m %f %b syslog facility = local3 EOF
学会查门路
很多状况下,咱们会先获取以后脚本的门路,而后一这个门路为基准,去找其余的门路。通常咱们是间接用pwd以期取得脚本的门路。
不过其实这样是不谨严的,pwd取得的是以后shell的执行门路,而不是以后脚本的执行门路。
正确的做法应该是上面这两种:
script_dir=$(cd $(dirname $0) && pwd)script_dir=$(dirname $(readlink -f $0 ))
该当先cd进以后脚本的目录而后再pwd,或者间接读取以后脚本的所在门路。
代码要简短
这里的简短不单单是指代码长度,而是只用到的命令数。原则上咱们该当做到,能一条命令解决的问题绝不用两条命令解决。这不仅牵涉到代码的可读性,而且也关乎代码的执行效率。
最最经典的例子如下:
cat /etc/passwd | grep rootgrep root /etc/passwd
cat命令最为人不齿的用法就是这样,用的没有任何意义,明明一条命令能够解决,他非得加根管道。。。
其实代码简短在还能某种程度上能保障效率的晋升,比方上面的例子:
#method1find . -name '*.txt' |xargs sed -i s/233/666/gfind . -name '*.txt' |xargs sed -i s/235/626/gfind . -name '*.txt' |xargs sed -i s/333/616/gfind . -name '*.txt' |xargs sed -i s/233/664/g#method1find . -name '*.txt' |xargs sed -i "s/233/666/g;s/235/626/g;s/333/616/g;s/233/664/g"
这两种办法做的事件都一样,就是查找所有的.txt后缀的文件并做一系列替换。前者是屡次执行find,后者是执行一次find,然而减少了sed的模式串。第一种更直观一点,然而当替换的质变大的时候,第二种的速度就会比第一种快很多。这里效率晋升的起因,就是第二种只有执行一次命令,而第一种要执行屡次。并且,巧用xargs命令,咱们还能够非常不便的进行并行化解决:
find . -name '*.txt' |xargs -P $(nproc) sed -i "s/233/666/g;s/235/626/g;s/333/616/g;s/233/664/g"
通过-P参数指定并行度,能够进一步放慢执行效率。
命令并行化
当咱们须要充分考虑执行效率时,咱们可能须要在执行命令的时候思考并行化。shell中最简略的并行化是通过”&”以及”wait”命令来做:
func(){ #do sth } for((i=0;i<10;i++))do func & done wait
当然,这里并行的次数不能太多,否则机器会卡死。略微正确的做法比较复杂,当前再探讨,如果图省事能够应用parallel命令来做,或者是用下面提到的xargs来解决。
全文本检索
咱们晓得,当咱们想在文件夹下所有的txt文件中检索某一个字符串(比方233)的时候,咱们可能会用相似这样的命令:
find . -name '*.txt' -type f | xargs grep 2333
很多状况下,这个命令会想咱们所想的找到对应的匹配行,然而咱们须要留神两个小问题。
find命令会符合要求的匹配文件名,然而如果文件名蕴含空格,这时候将文件名传给grep的时候就会有问题,这个文件就会被当成两个参数,这时候就要加一层解决,保障用空格离开的文件名不会被当成两个参数:
find . -type f|xargs -i echo '"{}"'|xargs grep 2333
有时候,文件的字符集可能跟终端的字符集不统一,这时候就会导致grep在搜寻时将文件当成二进制文件从而报binary file matches之类的问题。这时候要么用iconv之类的字符集转换工具将字符集进行切换,要么就在不影响查找的状况下对grep加-a参数,将所有文件看成文本文件:
find . -type f|xargs grep -a 2333
应用新写法
这里的新写法不是指有多厉害,而是指咱们可能更心愿应用较新引入的一些语法,更多是偏差代码格调的,比方
尽量应用func(){}来定义函数,而不是func{}
尽量应用[[]]来代替[]
尽量应用$()将命令的后果赋给变量,而不是反引号
在简单的场景下尽量应用printf代替echo进行回显
事实上,这些新写法很多性能都比旧的写法要弱小,用的时候就晓得了。
其余小tip
思考到还有很多系统的点,就不一一开展了,这里简略提一提。
门路尽量放弃绝对路径,绝多路径不容易出错,如果非要用相对路径,最好用./润饰
优先应用bash的变量替换代替awk sed,这样更加简短
简略的if尽量应用&& ||,写成单行。
比方[[ x > 2]] && echo x
当export变量时,尽量加上子脚本的namespace,保障变量不抵触
会应用trap捕捉信号,并在承受到终止信号时执行一些收尾工作
应用mktemp生成临时文件或文件夹
利用/dev/null过滤不敌对的输入信息
会利用命令的返回值判断命令的执行状况
应用文件前要判断文件是否存在,否则做好异样解决
不要解决ls后的数据(比方ls -l | awk ‘{ print $8 }’),ls的后果十分不确定,并且平台无关
读取文件时不要应用for loop而要应用while read
应用cp -r命令复制文件夹的时候要留神如果目标文件夹不存在则会创立,如果存在则会复制到该文件的子文件夹下
动态查看工具shellcheck
概述
为了从制度上保障脚本的品质,咱们最简略的想法大略就是搞一个动态查看工具,通过引入工具来补救开发者可能存在的常识盲点。
市面上对于shell的动态查看工具还真不多,找来找去就找到一个叫shellcheck的工具,开源在github上,有8K多的star,看上去还是非常靠谱的。咱们能够去他的主页理解具体的装置和应用信息。
装置
这个工具的对不同平台的反对力度都很大,他至多反对了Debian,Arch,Gentoo,EPEL,Fedora,OS X,openSUSE等等各种的平台的支流包管理工具。装置不便。具体能够参照装置文档
集成
既然是动态查看工具,就肯定能够集成在CI框架里,shellcheck能够十分不便的集成在Travis CI中,供以shell脚本为主语言的我的项目进行动态查看。
样例
在文档的Gallery of bad code里,也提供了十分具体的“坏代码”的规范,具备十分不错的参考价值,能够在闲下来的时候当成”Java Puzzlers“之类的书来读读还是很惬意的。
实质
不过,其实我感觉这个我的项目最最精髓的局部都不是下面的性能,而是他提供了一个十分十分弱小的wiki。在这个wiki里,咱们能够找到这个工具所有判断的根据。在这里,每一个检测到的问题都能够在wiki里找到对应的问题单号,他不仅通知咱们”这样写不好”,而且通知咱们”为什么这样写不好”,”咱们该当怎么写才好”,非常适合刨根问底党进一步钻研。
shell脚本写的溜,也是涨薪的必备技能哦!!如果本文对你有所帮忙与借鉴,请点个在看与转发分享反对一波哦!!