Shell编程之代码规范

1. 前言

1.1. 目的

代码编写规范,主要包括两部分,代码风格和最佳工程实践。在代码风格上,没有一种代码编写风格是最好的,更重要的是与已有项目代码风格保持一致,以提高项目团队整体对代码的可读性。在工程实践上,统一一些开发流程,提升团队的协作效率,另外就是最佳工程实践规范,以提高代码的性能、可靠性以及可读性。

1.2. 基本原则

  1. 参考主流Shell编程命名代码风格。
  2. 代码规范借鉴工具ShellCheck。
  3. 在性能足够的情况下,可读性优先考虑。

1.3. 预定义

  1. 小写驼峰命名,如pathName。
  2. 大写驼峰命名,如PathName。
  3. 大写加下划线,如MAX_DEV_CNT=32。
  4. 小写加下划线,如path_name=/dev/nvme0。

2. 代码风格

2.1. 文件头

必须使用#!/bin/bash指定bash解释器,因为这是应用最广泛的解释器。版权及作者信息默认也需要添加。

#!/bin/bash
################################################################ 
# Copyright 2022, xxxxxx Co. Ltd.
# All rights reserved.
# FileName:    case001.sh
# Description: first case for test.
# Author:      Michael
# http://www.xxxxxx.com 
# Revision: 1.0.0
#################################################################

2.2. 注释

尽量使用代码自注释,即用代码名来表达清楚。无法表达清楚的使用注释。注释应说明设计思路而不是描述代码的行为,代码的行为尽量依赖代码本身来表述清楚。

  1. 单行注释,#后面要空一格。
# Delete a file in a sophisticated manner.
  1. 函数注释
#######################################
# Get configuration directory.
# Globals:
#   SOMEDIR
# Arguments:
#   None
# Outputs:
#   Writes location to stdout
#######################################
get_dir() {
  echo "${SOMEDIR}"
}

#######################################
# Delete a file in a sophisticated manner.
# Arguments:
#  $1: File to delete, a path.
# Returns:
#   0 if thing was deleted, otherwise non-zero.
#######################################
del_thing() {
  rm "$1"
}

2.3. 缩进

tab键设置为4个空格,默认缩进为4个空格。

main() {
    # 缩进4个空格
    say="hello World."
    echo "${say}"
}

2.4. 函数

function定义,默认不需要加function修饰。函数统一放在源文件的全局变量之后,可执行代码之前,函数之间不放置可执行代码。代码功能比较少时,可以不定义main函数。

main() {
    echo "hello World."
    exit 0
}

2.5. 最大行数

代码一行的最大长度限定在120个字符左右。

2.6. 代码换行

  1. 长字符串换行
long_string="I am an exceptionally\
long string."
echo "${long_string}"
  1. 多个管道或逻辑操作(&& ||等)
# Long commands
command1 \
  | command2 \
  | command3 \
  | command4

2.7. 循环

让; do和; then和while for 以及if在同一行

for dir in "${dirs_to_cleanup[@]}"; do
  if [[ -d "${dir}/${ORACLE_SID}" ]]; then
    log_date "Cleaning up old files in ${dir}/${ORACLE_SID}"
    rm "${dir}/${ORACLE_SID}/"*
    if (( $? != 0 )); then
      error_message
    fi
  else
    mkdir -p "${dir}/${ORACLE_SID}"
    if (( $? != 0 )); then
      error_message
    fi
  fi
done

2.8. case语句

可选项中的多个命令应该被拆分成多行,模式表达式、操作和结束符 ;; 在不同的行。

case "${expression}" in
    a)
        variable="..."
        some_command "${variable}" "${other_expr}" ...
        ;;
    absolute)
        actions="relative"
        another_command "${actions}" "${other_expr}" ...
        ;;
    *)
        error "Unexpected expression '${expression}'"
        ;;
esac

如果表达式非常简单,可以使用简单模式:

verbose='false'
aflag=''
bflag=''
files=''
while getopts 'abf:v' flag; do
    case "${flag}" in
        a) aflag='true' ;;
        b) bflag='true' ;;
        f) files="${OPTARG}" ;;
        v) verbose='true' ;;
        *) error "Unexpected option ${flag}" ;;
    esac
done

2.9. 命名

  1. 文件名使用小写字母加下划线的形式,且以.sh结尾。
  2. 函数名使用小写字母加下划线的形式,包名使用::。
  3. 包中使用小写字母驼峰形式。
  4. 变量名使用小写字母加下划线的形式,局部变量尽量使用local修饰,减少变量名冲突。
  5. 常量使用大写字母加下划线形式,并且添加readonly修饰。
ysUtil::is_boot(){
    return 1
}

get_path() {
    echo "/dev/nvme0"
}

readonly MAX_PATH_LEN=256
test_dir() {
    local path_name
    path_name="$(get_path)" || return 1
    if [ ${#path_name} -gt $MAX_PATH_LEN ]; then 
        return 0
    fi
    
    return 1
}

2.10. 变量引用

  1. 针对参数或内置变量,可以不用{}。
  2. 针对字符串变量,默认添加{}。
  3. 针对数字变量,引用可以不加{}和字符串变量区别开。
# Special variables
echo $1 $2 $3
echo $? $!

# 当位置变量大于等于10,则必须有大括号:
echo "many parameters: ${10}"

# 当出现歧义时,必须有大括号:
# Output is "a0b0c0"
set -- a b c
echo "${1}0${2}0${3}0"

# 使用变量扩展赋值时,必须有大括号:
DEFAULT_MEM=${DEFUALT_MEM:-"-Xms2g -Xmx2g -XX:MaxDirectMemorySize=4g"}

# 其他常规变量的推荐处理方式:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read f; do
    echo "file=${f}"
done < <(ls -l /tmp)

2.11. 引用

引用通常情况下应遵循以下原则:
● 默认情况下推荐使用引号引用包含变量、命令替换符、空格或shell元字符的字符串
● 在有明确要求必须使用无引号扩展的情况下,可不用引号
● 字符串为单词类型时才推荐用引号,而非命令选项或者路径名
● 不要对整数使用引号
● 特别注意 [[ 中模式匹配的引号规则
● 在无特殊情况下,推荐使用 $@ 而非 $*

# '单引号' 表示禁用变量替换
# "双引号" 表示需要变量替换

# 1: 命令替换需使用双引号
flag="$(some_command and its args "$@" 'quoted separately')"

# 2:常规变量需使用双引号
echo "${flag}"

# 3:整数不使用引号
value=32
# 示例4:即便命令替换输出为整数,也需要使用引号
number="$(generate_number)"
echo "$value"

# 5:单词可以使用引号,但不作强制要求
readonly USE_INTEGER='true'

# 6:输出特殊符号使用单引号或转义
echo 'Hello stranger, and well met. Earn lots of $$$'
echo "Process $$: Done making \$\$\$."

# 7:命令参数及路径不需要引号
grep -li Hugo /dev/null "$1"

# 8:常规变量用双引号,ccs可能为空的特殊情况可不用引号
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}

# 9:正则用单引号,$1可能为空的特殊情况可不用引号
grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"}

# 10:位置参数传递推荐带引号的"$@",所有参数作为单字符串传递用带引号的"$*"
# content of t.sh
func_t() {
    echo num: $#
    echo args: 1:$1 2:$2 3:$3
}

func_t "$@"
func_t "$*"
# 当执行 ./t.sh a b c 时输出如下:
num: 3
args: 1:a 2:b 3:c
num: 1
args: 1:a b c 2: 3:

3. 最佳工程实践

3.1. 适用场景

Shell是一种Unix-like系统自带的脚本语言,在Windows上可以使用Cygwin等模拟器来运行。Shell的功能比较简单,其强大主要体现在与其配套的大量命令行工具。

  1. 需要调用其他应用程序,有许多文本操作,但是没有太多数据处理,那么Shell是一个好的选择。
  2. 如果有复杂的计算,或者对性能有强烈的追求,那么Shell不是好的选择。

3.2. 文件类型

Shell脚本只能以.sh为后缀名,并且脚本库文件必须设置为非可执行类型。

3.3. 文件编码

源文件编码格式为UTF-8。

3.4. Error输出到STDERR

所有的错误信息都应该输出到STDERR。

err() {
  echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2
}

if ! do_something; then
  err "Unable to do_something"
  exit 1
fi

3.5. 命令替换

使用新式语法$(command),不使用老式语法反引号,新语法可读性更高。

# good
var="$(command "$(command1)")"

# bad
var="`command \`command1\``"

3.6. 字符串匹配测试

优先使用[[ … ]],而不是[ … ], test,因为在 [[ 和 ]] 之间不会出现路径扩展或单词切分,所以使用 [[ … ]] 能够减少犯错,且 [[ … ]] 支持正则表达式匹配,而 [ … ] 不支持。

# 1:正则匹配,注意右侧没有引号
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
    echo "Match"
fi

# 2:严格匹配字符串"f*"(本例为不匹配)
if [[ "filename" == "f*" ]]; then
    echo "Match"
fi

# 3:[]中右侧不加引号将出现路径扩展,如果当前目录下有f开头的多个文件将报错[: too many arguments
if [ "filename" == f* ]; then
    echo "Match"
f

3.7. 字符串测试

# 推荐
if [[ "${my_var}" == "some_string" ]]; then
  do_something
fi

# 代码可行,但是不推荐
if [[ "${my_var}" = "val" ]]; then
  do_something
fi

# 使用-z或-n显式测试字符串
if [[ -z "${my_var}" ]]; then
  do_something
fi

# 不推荐
if [[ "${my_var}" ]]; then
  do_something
fi

# 代码可用,但是不推荐
if [[ "${my_var}" == "" ]]; then
  do_something
fi

3.8. 数字比较

# 推荐
if (( my_var > 3 )); then
  do_something
fi

# 推荐
if [[ "${my_var}" -gt 3 ]]; then
  do_something
fi

# 可行但是不推荐
if [[ "${my_var}" > 3 ]]; then
  # True for 4, false for 22.
  do_something
fi

3.9. 慎用管道连接while

管道连接while之后,命令是在子shell中执行,因为子Shell无法修改父Shell的变量,导致难以调试。
使用for循环代替。

# 不推荐
last_line='NULL'
your_command | while read line; do
    last_line="${line}"
done

# 推荐
total=0
for value in $(command); do
    total+="${value}"
done

3.10. 数组

使用新式语法赋值。

# 推荐
declare -a flags
flags=(--foo --bar='baz')
flags+=(--greeting="Hello ${name}")
mybinary "${flags[@]}"

# 不推荐
flags='--foo --bar=baz'
flags+=' --greeting="Hello world"'  # This won’t work as intended.
mybinary ${flags}

3.11. 文件加载

载入外部文件推荐使用source,代码可读性更好。

# 推荐
source base.sh

# 不推荐
. base.sh

3.12. 管道与参数

非必要情况,不使用管道传递参数,直接使用参数,效率更高。

# 推荐
grep "main" main.cpp
wc -l log.config

# 不推荐
cat main.cpp | grep "main"
cat log.config | wc -l

3.13. 数学计算

简单的数学计算可以使用(()),复杂的计算使用awk或bc。

# 推荐
(( i = 10 * j + 400 ))

# 可行,但是不推荐
i=$( expr 4 + 4 )

3.14. 检查命令返回值

需要检查命令返回值

if ! mv "${file_list[@]}" "${dest_dir}/"; then
  echo "Unable to move ${file_list[*]} to ${dest_dir}" >&2
  exit 1
fi

# Or
mv "${file_list[@]}" "${dest_dir}/"
if (( $? != 0 )); then
  echo "Unable to move ${file_list[*]} to ${dest_dir}" >&2
  exit 1
fi

3.15. 内部命令和外部命令

有一些命令,即支持外部命令工具,也支持Shell自带语法,更推荐使用自带内部命令,效率更高。

# 推荐使用内建的算术扩展
addition=$((${X} + ${Y}))
# 推荐使用内建的字符串替换
substitution="${string/#foo/bar}"

# 不推荐调用外部命令进行简单的计算
addition="$(expr ${X} + ${Y})"
# 不推荐调用外部命令进行简单的字符串替换
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"

4. 补充

  1. 推荐使用ShellCheck,VS Code可以下载ShellCheck插件,自动检测代码规范。
  2. 参考Google Shell Style Guild
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值