# Shell脚本书写规范

## **Shell脚本书写规范**

### **代码风格规范**

```shell
#!/usr/bin/env bash
```

### **代码注释**

```shell
#!/usr/bin/env bash
#########################################################
# Function :Rotate application logs                     #
# Platform :All Linux Based Platform                    #
# Version  :1.0                                         #
# Date     :2017-07-28                                  #
# Author   :Stephen.Shi                                 #
# Contact  :sp@meitu.com                                #
# Company  :MeiTu                                       #
#########################################################
```

### **参数规范**

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

```shell
# Uasge
help_msg(){
    echo "Usage: $0 <server_group_to_operate>"
}

# arg judge
if [[ $# -ne 1 ]]
then
    help_msg
else
    echo "參數不符合規範，過多"
fi
```

### **环境变量**

> 一般情况下我们会将一些重要的环境变量定义在开头，确保这些变量的存在。

```shell
source /etc/profile
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/apps/bin/"
```

### **变量的定义和引用**

```
1.  名称长的变量尽量使用驼峰写法或者使用小写字母+下划线的方式进行命名
2.  常量的定义推荐使用大写字母进行命名
3.  在引用变量时为了避免不必要的麻烦，尽量使用${变量名}的方式进行引用
```

### **缩进规矩**

> 缩进同一

### **命名标准**

1. 文件名规范，以.sh结尾，方便识别
2. 通过脚本名称最好能够直接读出脚本的用途，比如常用xx服务的启动脚本可命名为`start_xx.sh`，oo服务的监控脚本可命名为`oo_monit.sh`，告警脚本可名称为`abc_alert.sh`
3. 变量名字要有含义，不要拼错，尽量使用英文
4. 统一命名风格，用驼峰或者下划线连接，\*\*==推荐==\*\*使用小写字母加下划线的方式

### **编码统一**

> 在写脚本的时候尽量使用UTF-8编码，能够支持中文等一些奇奇怪怪的字符。 尽管脚本中可以写中文，但是在写注释以及打log的时候还是尽量英文，毕竟有些机器还是没有直接支持中文的，打出来可能会有乱码。

### **执行权限**

```shell
chmod +x xxxx.sh
```

### **日志和回显**

1. 一些debug级别的信息，在脚本调试结束后需要关闭。
2. 日志输出要带时间、要带时间、带时间，在写定时任务用的脚本时尤为重要
3. 日志的输出推荐使用tee -a ${log\_file}的方式，可以直接在main函数入口处添加日志输出。

> 脚本日志输出样例

```
logfile="/var/log/log_clean.log"

# define functions
function xx(){
    echo "xx"
}

function oo(){
    echo "oo"
}

# define main function
function main(){
    xx
    oo
}

# invoke main function
main|tee -a ${logfile}
```

### **太长要分行**

```
# 例如
./configure \
–prefix=/usr \
–sbin-path=/usr/sbin/nginx \
–conf-path=/etc/nginx/nginx.conf \
```

### **代码有效率**

```shell
sed -n '1p' file        # 会读取整个文件,效率慢
sed -n '1p;1q' file     # 只读取第一行，效率快
真正正确的用法应该是使用 `head -1 file`
```

### **勤用双引号**

```shell
#!/bin/sh
# 已知当前文件夹有一个a.sh的文件
var="*.sh"
echo $var
echo "$var

#### 他的运行结果如下：
a.sh
*.sh
```

### **函数入口**

```shell
#!/usr/bin/env bash

func1(){
    #do sth
}

func2(){
    #do sth
}

main(){
    func1
    func2
}

main "$@"

# 实现类似的main函数，使得脚本的结构化程度更好
# 另外在传递参数的时候可能会遇到参数接收位置的变化, 记得使用 `shift` 函数。
```

### **考虑作用域**

```shell
#!/usr/bin/env bash

var=1
func(){
    var=2
}
func
echo $var

# 输出结果： 2
因此，相比直接使用全局变量，使用 `local` `readonly` 这类的命令，
其次我们可以使用  `declare` 来声明变量。 这些方式都比使用全局方式定义要好。
```

### **巧用`heredocs`**

```shell
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
```

### **脚本路径**

```shell
script_dir=$(cd $(dirname $0) && pwd)         # cd进当前脚本的目录然后再pwd
script_dir=$(dirname $(readlink -f $0 ))      # 直接读取当前脚本的所在路径
```

### **代码要简短**

> 能一条命令解决的问题绝不用两条命令解决 ----> 牵涉到代码的可读性、执行效率

### **其他小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

### **小技巧**

> 封装函数有必要 使用静态变量声明 `readonly` 和 `local` 修饰变量 使用$()代替\`(反单引号) 使用\[\[]]代替\[]

```shell
bash -n test.sh            # 用-n对脚本进行语法检查, echo不是唯一的调试方法
bash -v test.sh            # 用-v跟踪脚本里的每个命令的执行
bash -x test.sh            # 用-x跟踪脚本里的每个命令的执行，并附加扩充信息
set -o nounset             # 遇到不存在的变量,终止脚本的执行
set -o errexit             # 遇到执行出错,终止脚本的执行
set -o verbose             # 打印读入shell的输入行
set -o xtrace              # 执行命令之前打印命令
date "+%Y-%m-%d %H:%M:%S"  # 输出当前时间：年月日 时分秒
read x                     # 和用户交互，读入输入变量
```
