• 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
    #################################################################
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.2. 注释

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

    1. 单行注释,#后面要空一格。
    # Delete a file in a sophisticated manner.
    
    • 1
    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"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    2.3. 缩进

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

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

    2.4. 函数

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

    main() {
        echo "hello World."
        exit 0
    }
    
    • 1
    • 2
    • 3
    • 4

    2.5. 最大行数

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

    2.6. 代码换行

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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:
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.5. 命令替换

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

    # good
    var="$(command "$(command1)")"
    
    # bad
    var="`command \`command1\``"
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.11. 文件加载

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

    # 推荐
    source base.sh
    
    # 不推荐
    . base.sh
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.12. 管道与参数

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

    # 推荐
    grep "main" main.cpp
    wc -l log.config
    
    # 不推荐
    cat main.cpp | grep "main"
    cat log.config | wc -l
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.13. 数学计算

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

    # 推荐
    (( i = 10 * j + 400 ))
    
    # 可行,但是不推荐
    i=$( expr 4 + 4 )
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.15. 内部命令和外部命令

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

    # 推荐使用内建的算术扩展
    addition=$((${X} + ${Y}))
    # 推荐使用内建的字符串替换
    substitution="${string/#foo/bar}"
    
    # 不推荐调用外部命令进行简单的计算
    addition="$(expr ${X} + ${Y})"
    # 不推荐调用外部命令进行简单的字符串替换
    substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    4. 补充

    1. 推荐使用ShellCheck,VS Code可以下载ShellCheck插件,自动检测代码规范。
    2. 参考Google Shell Style Guild
  • 相关阅读:
    GDB调试CoreDump文件
    私人云盘系统对比
    传输层——UDP
    华为OD机考:0030-0031-n*n数组中二进制的最大数、整数的连续自然数之和
    【bug日记】spring项目使用配置类和测试类操作数据库
    学习MyBatis过程中遇到的问题
    第四章 互联寄生
    tab切换(用jQuery实现)?
    GRS全球回收标准-未来趋势
    pygame退出时卡死
  • 原文地址:https://blog.csdn.net/feihe027/article/details/126484099