Shell脚本最佳实践

Shell脚本最佳实践

设置编码、缩进、文件命名和执行权限

使用utf-8编码;
统一使用tab缩进或空格缩进,不要混用;
文件名以.sh结尾,并且统一风格;
添加可执行权限:

chmod +x [bash_script.sh]

最后,在所有输出完毕后,添加一个空行。

指定默认解释器

也就是不要省略脚本第一行的shebang,一般默认是bash:

#!/bin/bash

或者更为通用一些:

#!/usr/bin/env bash

本机可用的shell解释器,可以通过以下命令查看:

cat /etc/shells

设置Shell环境

设置命令回显:

set -x

shell默认设置不够友好,我们希望予以加强。

# 遇到未声明的变量则报错停止
set -u
# 遇到执行错误则停止
set -e

由于set -e对管道命令无效,管道命令其中一步失败则中止,需要使用:

set -o pipefail

我们将这三条合并,构成 bash strict mode,添加在bash脚本的开始位置:

set -euo pipefail

因为这里都是shell环境设置,所以也可以在执行脚本的时候来使用:

bash -euo pipefail [bash_sctipt.sh]

总是使用main函数包裹执行体

main() {
      func1 param1 param2
      func2 param
}
main "$@"

与python类似,shell不需要函数入口,可以从第一条指令开始执行。但是为了可读性和方便调试,我们总是写一个命名为main的函数来作为全局入口。

变量

1)环境变量的设置和取消:

# 设置环境变量
export SKIP_BFS=1
# 取消环境变量
unset SKIP_BFS

注意,由于前文启用了strict mode,受set -u影响,脚本中使用未设置的环境变量,会报unbound variable错误。
可以通过-v来检测是否设置了环境变量:

if [[ -v SKIP_BFS ]]; then
      echo 'environment variable SKIP_BFS is set'
fi

2)局部变量
shell变量默认全局作用域,这一点与JavaScript类似,函数内声明局部变量,应该添加local关键字。

3)使用变量时,总是用花括号和双引号把变量包起来,例如:

# 带空格的路径
cp -r "${src_dir}" "${dest_dir}"

不适用双引号包裹变量的话,路径有空格会被作为两个参数来处理,从而导致很严重的bug,用"$var"这种写法,避免了这个问题。
花括号则是避免避免变量名和下划线的拼接处出现歧义的问题。

条件判断

字符比较和文件测试使用双方括号 [[ ]],并在每个变量和运算符以及和括号之间加入一个空格,例如:

if [[ $# > 1 ]] || [[ $# == 1 && $1 != 'PC' && $1 != 'server' ]]; then
      echo 'Invalid commandline arguments, you should use `./run.sh` or `./run.sh PC` or `./run.sh server`'
      exit 1
fi

其中,$#用于获取命令行参数个数,$N用于获取第N个命令行参数,参数$0指的是脚本文件名。
相比单方括号,双方括号的优势在于可以直接使用比较运算符> < == !=等,而不是必须使用-gt -lt -eq -ne;此外双方括号可以使用&& ||来表达与和或,而不用必须写-a -o这种难以记忆的写法,并且拥有逻辑短路的功能。因此,强烈建议使用双方括号取代单方括号作为作为条件判断语句。

数字的比较应该使用双小括号 (( )),并且不需要空格分隔各值和运算符。例如判断正在运行的进程个数:

running=$(ps -aux -r | wc -l)
if (( ${running} > 5 )); then
      echo "${running} processes running, please handle this problem. exit."
      exit 1;
fi

在双方括号中进行数字的比较也是可以的,但是直接使用比较运算符> < == !=等得到的常常是错误的结果,使用-gt -lt -eq -ne得到的总是正确的但难以记忆。使用双小括号则可以直接使用比较运算符进行判断。

使用文件前做好异常处理

# 判断文件夹存在
if [[ ! -d 'src' ]]; then
      echo 'src dir not found'
      exit 1
fi
# 判断普通文件存在
if [[ ! -f 'a.txt' ]]; then
      touch 'a.txt'
fi
# 判断可执行文件存在并且可执行
if [[ ! -x "$(command -v java)" ]]; then
      echo 'java is not installed, or not execuatable'
fi

注意cp -r命令,在文件夹不存在时回创建文件夹并复制,而当文件夹存在时,会复制到子文件夹内。

循环语句

提倡使用for-in循环

# C风格
for (( i=0; i<10; i++)); do
      // echo $i
done
# for-in
for i in $(seq 0 9); do
      // echo $i

和 if 语句的 then 一样,for 语句的 do 也紧跟在语句后面,不单独占一行,这样显得比较紧凑。同样不要忘记加分号。

这里补充说明一下seq语句用法,注意与python做好区分:

# 单参数,输出 1 2 3 4
$(seq 4)
# 双参数,输出 2 3 4 5
$(seq 2 5)
# 三参数,输出 8 6 4 2
$(seq 8 -2 1)

这里三参数情况时的增量参数,可以正可以负,也可以是小数。
更多用法可参考Bash Range: How to iterate over sequences generated on the shell

${arr[@]}${arr[*]} 进行列表循环

$*$@ 的相同点都是引用所有参数;不同点则只有在双引号中体现出来。假设在脚本运行时写了三个参数 1、2、3,,则 $* 等价于 "1 2 3"(传递了一个参数),而 $@ 等价于 "1" "2" "3"(传递了三个参数)
单个列表元素迭代:

arr=(1 3 5 a)
for s in ${arr[@]}; do
      echo $s
done

多个列表合并迭代:

arr1=(1 3 5 a)
arr2=(2 4 6 b)
for s in ${arr1[@]} ${arr2[@]}; do
      echo $s
done

注意,花括号不可省略。
如果需要进行函数传参,则需要使用使用 $*,并且在传参时使用双引号 " 把列表参数包起来作为一个参数整体传入。示例如下:

deltas="0.1 0.2 0.3"
run() {
    params=$1
    for x in ${params[*]}; do
        echo $x
    done
}
run "${deltas[*]}"

注意,函数内参数列表不能用引号,函数调用处引号不可少。原因在于,函数外是要把列表作为一个参数整体传入(而不是分成多个参数传入),函数内是把列表拆成多个元素依次遍历。

有时候,我们希望对列表中的元素整体加前缀或者加后缀,在Makefile里可以很方便地调用addprefixaddsuffix两个内置函数来完成,在bash里则需要使用Bash parameter expansion

# addprefix
for s in ${arr[@]/#/PREFIX}; do
      echo $s
done
# addsuffix
arr_suffix=${arr[@]/%/SUFFIX}
echo ${arr_suffix}

使用$()而不是反引号获取表达式的值

如for-in:

# 建议使用 $(seq lb ub) 而不是 `seq lb ub` 获取范围
for i in $(seq 0 10) do 
      echo $i
done

使用(())bc进行数学运算

shell默认的都是文本操作,所以a=$b+$c并不能把两个数进行求和,需要数学运算的话,应该明确标明。
分两种情况,整数运算和浮点运算:
整数运算建议使用(()),不建议适用[]letexpr:

(( a = $b + $c ))
# 或者
a=$( b + c ))

浮点运算可以用bc:

echo "$b + $c" | bc
# 或者
bc <<< "$b + $c"

使用 /dev/null 过滤输出信息

[expr] > /dev/null 2>&1

命令解释:重定向到空设备,并把标准错误输出stderr也重定向为stdout。
注意,2>&1应该总是放在命令的末尾。

获取脚本所在目录

有时候,需要适用脚本对同一份代码仓库下其他文件夹内的文件进行操作,如codegen、format、validate等工作。此时需要的是相对本脚本的路径,与调用脚本时的路径无关,所以需要先行获取脚本所在路径(绝对路径):

# 在 `$()` 里面执行 cd 命令不会改变当前工作路径
readonly __DIR__=$(cd $(dirname $0) && pwd)
echo $__DIR__

参考https://john-yuan.org/blog/how-to-get-the-dir-of-the-current-shell-script.html

case语句等

TBD

进一步阅读

Google Bash风格指南
阮一峰 Bash 脚本教程

posted @ 2020-08-23 14:48  与MPI做斗争  阅读(951)  评论(0编辑  收藏  举报