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脚本的开始位置:
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)
这⾥三参数情况时的增量参数,可以正可以负,也可以是⼩数。
更多⽤法可参考
⽤${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⾥可以很⽅便地调⽤addprefix和addsuffix两个内置函数来完成,在bash ⾥则需要使⽤:
# 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并不能把两个数进⾏求和,需要数学运算的话,应该明确标明。
分两种情况,整数运算和浮点运算:
整数运算建议使⽤(()),不建议适⽤[]、let、expr:
(( a = $b + $c ))
shell最简单脚本# 或者
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__
case语句等
TBD
进⼀步阅读

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。