关于运维:程序员必备神器Shell-脚本编程最佳实践

1次阅读

共计 7662 个字符,预计需要花费 20 分钟才能阅读完成。

作者: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 1
fi

变量和魔数

个别状况下咱们会将一些重要的环境变量定义在结尾,确保这些变量的存在。

source /etc/profile
export 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' file
sed -n '1p;1q' file

他们的作用一样,都是获取文件的第一行。然而第一条命令会读取整个文件,而第二条命令只读取第一行。当文件很大的时候,仅仅是这样一条命令不一样就会造成微小的效率差别。

当然,这里只是为了举一个例子,这个例子真正正确的用法应该是应用 head -n1 file 命令。。。

勤用双引号

简直所有的大佬都举荐在应用”$”来获取变量的时候最好加上双引号。

不加上双引号在很多状况下都会造成很大的麻烦,为什么呢?举一个例子:

#!/bin/sh
#已知以后文件夹有一个 a.sh 的文件
var="*.sh"
echo $var
echo "$var"

他的运行后果如下:

a.sh
*.sh

为啥会这样呢?其实能够解释为他执行了上面的命令:

echo *.sh
echo "*.sh"

在很多状况下,在将变量作为参数的时候,肯定要留神下面这一点,认真领会其中的差别。下面只是一个十分小的例子,理论利用的时候因为这个细节导致的问题切实是太多了。。。

巧用 main 函数

咱们晓得,像 java,C 这样的编译型语言都会有一个函数入口,这种构造使得代码可读性很强,咱们晓得哪些间接执行,那些是函数。然而脚本不一样,脚本属于解释性语言,从第一行间接执行到最初一行,如果在这当中命令与函数糅杂在一起,那就十分难读了。

用 python 的敌人都晓得,一个合乎规范的 python 脚本大体上至多是这样的:

#!/usr/bin/env python
def 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=VAR2
eval $VAR1=233
echo $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 root
grep root /etc/passwd

cat 命令最为人不齿的用法就是这样,用的没有任何意义,明明一条命令能够解决,他非得加根管道。。。

其实代码简短在还能某种程度上能保障效率的晋升,比方上面的例子:

#method1
find . -name '*.txt' |xargs sed -i s/233/666/g
find . -name '*.txt' |xargs sed -i s/235/626/g
find . -name '*.txt' |xargs sed -i s/333/616/g
find . -name '*.txt' |xargs sed -i s/233/664/g

#method1
find . -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 脚本写的溜,也是涨薪的必备技能哦!!如果本文对你有所帮忙与借鉴,请点个 在看 转发分享 反对一波哦!!

正文完
 0