shell的学习建议

在做 Linux 系统相关的运维工作中,经常会编写shell脚本来完成服务的一键配置,定时维护等任务。shell 脚本严格意义上讲并不是编程语言。很多人在写脚本的时候,想到哪里就写到哪里,多人协作的时候代码很难得到规范和统一。为了更高效地写 shell 脚本,结合自己的经验,查阅了一些资料,罗列了一些建议。

规范自己的代码风格

根据自己的使用习惯,写脚本时遵循一致的代码风格。最简单的一个就是缩进如果习惯了是4个空格那就一直保持,最好不要是今天写的时候缩进是4个空格,明天写的时候成了两个空格或一个制表符。

开头的解释器

  • shebang 其实就是在很多脚本的第一行出现的以#!开头的注释,指明了当我们没有指定解释器的时候默认的解释器,一般是下面这样:
1
#!/bin/bash
  • 直接使用./a.sh 来执行这个脚本的时候,如果没有 shebang,那么它就会默认用 $SHELL 指定的解释器,否则就会用 shebang 指定的解释器

  • 上面的写法可能不太具备适应性,一般我们会用下面的方式来指定,而且这也是比较推荐的使用方式

1
#!/usr/bin/env bash
  • 解释器分很多种,可以用下面的命令查看本机支持的解释器:
1
2
3
4
5
6
7
[user1@test ~]$ cat /etc/shells 
/bin/sh
/bin/bash
/sbin/nologin
/bin/tcsh
/bin/csh
/bin/ksh

内容做到注释

  • 不管是其他开发语言也好,shell这种脚本语言也要,在写代码的过程中养成良好的注释习惯。没有注释的情况下,在三四个月之后你可能回头看自己的代码时不知道当初的自己是如何想的,以及不知道代码实现了什么功能;另一方面注释可以提高可读性,和别人协作的时候,别人看了也不至于头大。

  • 注释一般包括以下几个方面:

    • shebang
    • 脚本的参数
    • 脚本的用途
    • 脚本的注意事项
    • 脚本的写作时间,作者,版权等
    • 各个函数前的说明注释
    • 一些较复杂的单行命令注释

参数要规范

当我们的脚本需要接受参数的时候,一定要先判断参数是否合乎规范,并给出合适的回显,方便使用者了解参数的使用。至少需要判断下参数的个数:

1
2
3
4
5
6
7
8
9
10
if test $# -le 1;then

echo "Usage : $(basename $0) srcip < destip | dest domainname >"
exit 0

else

echo "....."

fi

环境变量的定义

一般情况下我们会将一些重要的环境变量定义在开头,确保这些变量的存在。最典型的应用就是,当我们本地安装了很多 java 版本时,我们可能需要指定一个 java 来用。那么这时我们就会在脚本开头重新定义 JAVA_HOME 以及 PATH 变量来进行控制。

1
2
3
4
5
6
7

# load profile
source /etc/profile

# Set up a default search path.
PATH="/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/sbin:/usr/local/bin:/root/bin"
export PATH

缩进要规矩

  • 正确的缩进是很重要的,尤其是在写函数的时候,否则我们在阅读的时候很容易把函数体跟直接执行的命令搞混。

  • 对于 shell 脚本,因为很多需要缩进的地方 (比如 iffor 语句) 都不长,很多人因此都懒得去缩进,而且很多人不习惯用函数,导致缩进功能被弱化。

  • 常见的缩进方法主要有”soft tab” 和”hard tab” 两种

    • soft tab:就是使用 n 个空格进行缩进 (n 通常是 2 或 4)
    • hard tab: 指的就是制表符\t
  • 根据自己的使用习惯选择适合自己的缩进方式,并一直保持下去

  • 对于 if 和 for 语句之类的,我们最好不要把 then,do 这些关键字单独写一行,这样看上去比较丑。下面是示例
1
2
3
for i in {1..10};do
echo ${i}
done
1
2
3
4
5
6
7
8
# Check network
dstname="mirrors.163.com"
if (ping ${dstname} -c 3 -i 0.01 -w 2 -q 1>/dev/null 2>&1)then
echo "Network is OK "
else
echo "Network is unreachable "
exit 2
fi

脚本和变量命名要标准

遵循合理的规范:

  • 文件名规范:以. sh 结尾,方便识别
  • 变量名规范:
    • 最好见名知意
    • 统一命名风格, 一般用小写字母加下划线(file_path),或使用驼峰语法(filePath)

编码要统一

  • 在写脚本的时候尽量使用 UTF-8 编码,能够支持中文等一些奇奇怪怪的字符。虽然能写中文,但是在写注释以及打 log 的时候还是尽量英文,因为很多机器还是没有直接支持中文的,打出来可能会有乱码。

  • 尤其需要注意一点,当我们是在 windows 下用 utf-8 编码来写 shell 脚本的时候,一定要注意这个 utf-8 是否是有 BOM 的。默认情况下 windows 判断 utf-8 格式是通过在文件开头加上三个 EF BB BF 字节来判断的,但是在 Linux 中默认是无 BOM 的。因此如果我们是在 windows 下写脚本的时候,一定要注意将编码改成 Utf-8 无 BOM,一般用 notepad++ 之类的编辑器都能改。否则,在 Linux 下运行的时候就会识别到开头的三个字符,从而报一些无法识别命令的错。

使用统一的执行方式

执行shell脚本的方式大致有如下几种

1
bash /tmp/test.sh
1
/tmp/test.sh
1
./test.sh

其中后面两种是需要脚本有执行权限的,这也是建议的执行方式。不加执行权限导致无法直接执行。

日志回显

  • 日志的重要性不必多说,能够方便我们回头纠错,在大型的项目里是非常重要的。
  • 如果这个脚本是供用户直接在命令行使用的,那么我们最好还要能够在执行时实时回显执行过程,方便用户掌控。
  • 为了提高用户体验,我们可以在回显中添加一些特效,比如颜色、闪烁等

不在脚本中使用密码

安全很重要,不要把密码硬编码在脚本里,尤其是当脚本托管在类似 Github 这类平台中时,明文密码就暴露在互联网中了。

代码太长要分行

在源码包编译安装软件时,参数可能会很长,为了保证较好的阅读体验,我们可以用反斜杠来分行:

1
2
3
4
./configure \   
–prefix=/usr \
–sbin-path=/usr/sbin/nginx \
–conf-path=/etc/nginx/nginx.conf \

注意:\前要有个空格

编码的细节和规范

使用新的语法

这里的新语法不是指有多厉害,而是指我们可能更希望使用较新引入的一些语法,更多是偏向代码风格的,比如

  • 尽量使用 func(){}来定义函数,而不是func{}
  • 尽量使用[[]]来代替[]
  • 尽量使用$()将命令的结果赋给变量,而不是反引号
  • 在复杂的场景下尽量使用 printf代替 echo进行回显

事实上,这些新写法很多功能都比旧的写法要强大,用的时候就知道了。

让自己的代码更简短

这里的简短不单单是指代码长度,而是用到的命令个数。这不仅牵涉到代码的可读性,而且也关乎代码的执行效率。

  • 原则上应该做到:
    • 能一条命令解决的问题绝不用两条命令解决
    • 能直接读取文件就不要用管道
    • 命令能少尽可能少,管道并不是越多越好
  • 题外话:你用的管道太多会让别人感觉你很low
  • 示例
1
2
cat /etc/passwd | grep root   
grep root /etc/passwd
  • 其实代码简短在还能某种程度上能保证效率的提升,比如下面的例子:
1
2
3
4
5
#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
1
2
#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的模式串。第一种可读性更好一点,但是当替换的量变大的时候,第二种的速度就会比第一种快很多。这里效率提升的原因,就是第二种只要执行一次命令,而第一种要执行多次。

让自己的代码更有效率

在使用命令的时候要了解命令的具体做法,尤其当数据处理量大的时候,要时刻考虑该命令是否会影响效率。
比如下面的两个 sed 命令:

1
2
3
4
5
6
sed -n '1p' file
sed -n '1p;1q' file

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

在敏感操作时变量引用要严谨

对一个值为路径的变量进行删除、移动操作时,一定要先判断该变量是否为空

  • 示例
1
2
3
path=/server/backup
find $path -name "*.tar.gz" -type f | xargs rm -f
# 一旦变量为空,则会删除当前目录
  • 避免方法

    • 删除操作之前,切换到一个临时目录
    • 使用变量展开,若变量未定义或者为空则赋一个值给这个变量,
  • 示例

    1
    [ $RETVAL = 0 ] && rm -f ${pidfile:=/tmp/test} /var/lock/subsys/${prog:=/tmp/test}

勤用双引号

  • 示例1
1
2
3
4
5
#!/bin/bash   
# 已知当前文件夹有一个a.sh的文件
var="*.sh"
echo $var
echo "$var"
  • 运行结果如下:
1
2
a.sh
*.sh
  • 这是因为shell对*进行了解释,变成了下面这样
1
2
echo *.sh
echo "*.sh"
  • 示例2
1
2
3
#!/bin/bash
arg1=$1
[ ${arg1} == "0" ] && echo "0" || echo 1
  • 直接执行bash test.sh,不加参数的情况下就会报错,因此需要使用双引号:
1
2
3
#!/bin/bash
arg1=$1
[ "${arg1}" == "0" ] && echo "0" || echo 1

在很多情况下,在将变量作为参数的时候,一定要注意双引号的使用,实际应用的时候由于这个细节导致的问题实在是太多了

使用main函数

像 java,C 这样的编译型语言都会有一个函数入口,这种结构使得代码可读性很强,我们知道哪些直接执行,哪些是函数。但是脚本不一样,脚本属于解释性语言,从第一行直接执行到最后一行,如果在这当中命令与函数糅杂在一起,那就非常难读了。

用 python 的朋友都知道,一个合乎标准的 python 脚本大体上至少是这样的:

1
2
3
4
5
6
7
8
#!/usr/bin/env python   
def func1():
pass
def func2():
pass
if __name__=='__main__':
func1()
func2()

他用一个很巧妙的方法实现了我们习惯的 main 函数,使得代码可读性更强

在 shell 中,我们也有类似的小技巧:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env bash   
func1(){
#do sth
}
func2(){
#do sth
}
main(){
func1
func2
}
main "$@"

我们可以采用这种写法,同样实现类似的 main 函数,使得脚本的结构化程度更好。

理清作用域

shell 中默认的变量作用域都是全局的,比如下面的脚本:

1
2
3
4
5
6
7
#!/usr/bin/env bash   
var=1
func(){
var=2
}
func
echo $var

他的输出结果就是 2 而不是 1,这样显然不符合我们的编码习惯,很容易造成一些问题。

因此,相比直接使用全局变量,我们最好使用 local readonly 这类的命令,其次我们可以使用 declare 来声明变量。这些方式都比使用全局方式定义要好。

  • 下面是做了修整的示例
1
2
3
4
5
6
7
#!/usr/bin/env bash   
var=1
func(){
local var=2
}
func
echo $var

注意事项

  • 路径尽量保持绝对路径,绝多路径不容易出错,如果非要用相对路径,最好用./修饰
  • 优先使用 bash 的变量替换代替 awk sed,这样更加简短,比如
1
2
3
a=131341
echo ${a/3/h}
echo ${a//3/h}
  • 简单的if尽量使用 && ||,写成单行。比如
1
[[ x -gt 2]] && echo x
  • 当 export 变量时,尽量加上子脚本的 namespace,保证变量不冲突
  • 会使用 trap 捕获信号,并在接受到终止信号时执行一些收尾工作
  • 使用 mktemp 生成临时文件或文件夹来避免和他人冲突
  • 利用 / dev/null 结合输入输出重定向来过滤不友好的输出信息
  • 利用命令的返回值判断命令的执行情况
  • 使用文件前要判断文件是否存在,否则做好异常处理
  • 不要处理 ls 后的数据 (比如ls -l | awk '{ print $8}'),ls 的结果非常不确定,并且和平台有关
  • 读取文件时不要使用 for loop 而要使用 while read
  • 写脚本一定先测试再到生产环境
有钱任性,请我吃包辣条
0%