欢迎光临
我们一直在努力

Google 团队 Shell 编码风格指南-中文版

文章目录

  • Shell 文件和解释器调用
  • 文件名的通配符扩展
  • 修订版本 2.02

    由许多谷歌员工创作、修订和维护。

    注:翻译内容可能因语言特性而略有调整,但力求保持准确、流畅和地道。

    目录

    背景

    选择使用哪种 Shell

    Bash 是唯一允许用于可执行文件的 shell 脚本语言。

    可执行文件必须以 #!/bin/bash 和最少数量的标志开头。使用 set 命令设置 shell 选项,以便在调用脚本时使用 bash script_name 不会破坏其功能。

    将所有可执行的 shell 脚本限制为 bash 可以确保我们拥有一种一致的 shell 语言,它安装在我们所有的机器上。

    唯一的例外是在被编写的代码所要求的情况下。其中一个例子是 Solaris SVR4 软件包,它要求纯 Bourne shell 用于任何脚本。

    何时使用 Shell

    Shell 只应用于编写小型实用工具或简单的包装脚本。

    虽然 shell 脚本不是一种开发语言,但在 Google 中被用于编写各种实用程序脚本。这个风格指南更多地是对其使用的认可,而不是建议广泛使用。

    一些准则:

    • 如果你主要在调用其他实用工具,并且进行的数据操作相对较少,那么 shell 是完成任务的可接受选择。
    • 如果性能很重要,请使用其他非 shell 的工具。
    • 如果你正在编写的脚本超过 100 行,或者使用了非常规的控制流逻辑,应该立即用一种更结构化的语言进行重写。要记住,脚本会不断增长。早点重写你的脚本,以避免以后更耗时的重写。
    • 在评估代码复杂性时(例如,决定是否切换语言),要考虑代码是否容易由其作者以外的人维护。

    Shell 文件和解释器调用

    文件扩展名

    可执行文件应该没有扩展名(强烈推荐)或使用 .sh 扩展名。库文件必须使用 .sh 扩展名,而且不应该是可执行的。

    在执行程序时不需要知道它是用什么语言编写的,而且 shell 不需要扩展名,因此我们更倾向于不在可执行文件中使用扩展名。

    然而,对于库文件来说,知道它是用什么语言编写的很重要,有时需要在不同的语言中有类似的库。这允许具有相同目的但使用不同语言编写的库文件具有相同的名称,除了语言特定的后缀。

    SUID/SGID

    在 shell 脚本上禁止使用 SUID 和 SGID。

    由于 shell 存在太多的安全问题,几乎不可能提供足够的安全性来允许 SUID/SGID。尽管 bash 在运行 SUID 方面做得很困难,但在某些平台上仍然有可能,这就是为什么我们明确禁止使用它的原因。

    如果需要提供提升的访问权限,请使用 sudo

    环境

    标准输出 vs 标准错误

    所有错误消息应该输出到 STDERR

    这样可以更容易地区分正常状态和实际问题。

    建议使用一个函数来打印错误消息以及其他状态信息。

    err() {
      echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2
    }
    
    if ! do_something; then
      err "无法执行某事"
      exit 1
    fi

    注释

    文件头部注释

    每个文件都应该以描述其内容的说明开头。

    每个文件必须有一个顶级注释,包括对其内容的简要概述。版权声明和作者信息是可选的。

    示例:

    #!/bin/bash
    #
    # 执行 Oracle 数据库的热备份。

    函数注释

    任何既不明显又不简短的函数都必须进行注释。库中的任何函数都必须进行注释,无论其长度或复杂性如何。

    通过阅读注释(和提供的自助信息,如果有的话),他人应该能够学习如何使用你的程序或使用你的库中的函数,而无需阅读代码。

    所有函数注释都应该使用以下方式描述预期的 API 行为:

    • 函数的描述。
    • Globals(全局变量):所使用和修改的全局变量列表。
    • Arguments(参数):所使用的参数。
    • Outputs(输出):输出到 STDOUT 或 STDERR。
    • Returns(返回值):除了最后一个运行的命令的默认退出状态以外的返回值。

    示例:

    #######################################
    # 清理备份目录中的文件。
    # Globals:
    #   BACKUP_DIR
    #   ORACLE_SID
    # Arguments:
    #   无
    #######################################
    function cleanup() {
      …
    }
    
    #######################################
    # 获取配置目录。
    # Globals:
    #   SOMEDIR
    # Arguments:
    #   无
    # Outputs:
    #   将位置写入标准输出
    #######################################
    function get_dir() {
      echo "${SOMEDIR}"
    }
    
    #######################################
    # 以复杂的方式删除文件。
    # Arguments:
    #   要删除的文件路径。
    # Returns:
    #   如果成功删除,则返回 0;出错时返回非零值。
    #######################################
    function del_thing() {
      rm "$1"
    }

    实现注释

    对于代码中复杂、不明显、有趣或重要的部分进行注释。

    这遵循了谷歌的通用编码注释实践。不要对所有内容都进行注释。如果存在复杂的算法或者你正在做一些不寻常的事情,只需在其附上简短的注释。

    待办注释

    对于临时的、短期的解决方案或足够好但不完美的代码,使用 TODO 注释。

    这与 C++ 指南 中的约定相匹配。

    TODO 注释应该使用全大写的字符串 TODO,后面跟着具有与 TODO 引用的问题最佳上下文的人的姓名、电子邮件地址或其他标识符。主要目的是拥有一个一致的 TODO,可以通过搜索找出如何在请求时获取更多详细信息。TODO 不是一个承诺,表明被引用的人会修复问题。因此,当创建 TODO 时,通常会使用你自己的名字。

    示例:

    # TODO(mrmonkey): 处理不太可能的边界情况(错误 ####)

    格式化

    尽管在修改文件时应遵循已有的样式,但对于任何新的代码,需要遵循以下要求。

    缩进

    缩进2个空格。不使用制表符。

    在块之间使用空行以提高可读性。缩进是两个空格。无论如何,都不要使用制表符。对于现有文件,请保持对现有缩进的忠实。

    行长度和长字符串

    最大行长度为80个字符。

    如果必须编写超过80个字符的字符串,则应使用here文档或内嵌换行符进行操作,如果可能的话。必须超过80个字符并且不能明智地拆分的字面字符串是可以的,但强烈建议寻找缩短字符串的方法。

    # 使用 'here document'
    cat <

    管道

    如果管道不全都适合一行,则将其拆分为每行一个。

    如果整个管道适合一行,则应在一行上。

    如果不适合,则应在每行一个管道分段上拆分,换行处放置管道,并在下一个管道段的开头缩进2个空格。这适用于使用 | 组合的命令链,以及使用 ||&& 的逻辑组合。

    # 适合一行
    command1 | command2
    
    # 长命令
    command1 
      | command2 
      | command3 
      | command4

    循环

    ; do; then 放在与 whileforif 同一行。

    在Shell中的循环与大括号的声明函数类似,因此在声明函数时遵循相同的原则。即 ; then; do 应与 if/for/while 放在同一行上。else 应单独一行,关闭语句应该单独一行,并与开放语句在垂直方向上对齐。

    示例:

    # 如果在函数内部,请考虑将循环变量声明为
    # 本地变量,以避免其泄漏到全局环境中:
    # local dir
    for dir in "${dirs_to_cleanup[@]}"; do
      if [[ -d "${dir}/${ORACLE_SID}" ]]; then
        log_date "在 ${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

    Case语句

    • 用2个空格缩进选项。
    • 一行选项需要在模式的右括号之后和 ;; 之前加一个空格。
    • 长或多命令选项应该拆分为多行,模式、操作和 ;; 放在单独的行上。

    匹配表达式从 caseesac 缩进一个级别。多行操作再缩进一个级别。通常情况下,不需要引用匹配表达式。模式表达式不应该在开括号之前。避免使用 ;&;;& 符号。

    case "${expression}" in
      a)
        variable="…"
        some_command "${variable}" "${other_expr}" …
        ;;
      absolute)
        actions="relative"
        another_command "${actions}" "${other_expr}" …
        ;;
      *)
        error "意外的表达式 '${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 "意外的选项 ${flag}" ;;
      esac
    done

    变量扩展

    按优先级顺序:保持与现有代码一致;对变量加引号;优先使用 "${var}" 而不是 "$var"

    这些是强烈推荐的准则,但并非强制性规定。尽管如此,这并不意味着这只是一个建议,不是强制性的,所以不能轻视或忽视它。

    它们按优先级顺序列出。

    • 保持与现有代码的一致性。

    • 引用变量,参见下面的引用

    • 不要使用花括号将单字符shell特殊符号/位置参数括起来,除非绝对必要或避免深度混淆。

    优先使用花括号将所有其他变量括起来。

    # 推荐的用例部分。
    
    # 优选的风格,用于 'special' 变量:
    echo "位置参数: $1" "$5" "$3"
    echo "特殊变量: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ $=$$ …"
    
    # 需要花括号的情况:
    echo "许多参数: ${10}"
    
    # 避免混淆的花括号用法:
    # 输出为 "a0b0c0"
    set -- a b c
    echo "${1}0${2}0${3}0"
    
    # 其他变量的首选风格:
    echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
    while read -r f; do
      echo "文件=${f}"
    done < <(find /tmp)
    # 不推荐的用例部分。
    
    # 未引用的变量、未使用花括号的变量、花括号将单字符shell特殊变量括起来。
    echo a=$avar "b=$bvar" "PID=${$}" "${1}"
    
    # 混淆的用法:这会被展开为 "${1}0${2}0${3}0",
    # 而不是 "${10}${20}${30}
    set -- a b c
    echo "$10$20$30"

    注意:在 ${var} 中使用花括号 不是 引用的一种形式。引号必须同时使用。

    引用

    • 总是引用包含变量、命令替换、空格或shell元字符的字符串,除非需要小心不带引号的扩展,或者是一个Shell内部整数(见下一点)。
    • 使用数组来安全地引用列表元素,尤其是命令行标志。参见下面的数组
    • 可选地引用Shell内部、只读的特殊整数变量:$?$#$$$!(man bash)。出于一致性考虑,更喜欢引用“命名”的内部整数变量,例如 PPID 等。
    • 更喜欢引用是“单词”(而不是命令选项或路径名)的字符串。
    • 绝不引用文字整数。
    • 要注意在 [[ … ]] 中进行模式匹配的引用规则。参见下面的Test、[… ][[… ]] 部分。
    • 使用 "$@",除非有特定原因使用 $*,例如在消息或日志中简单地附加参数。
    # '单引号' 表示不需要替换。
    # "双引号" 表示需要替换。
    
    # 简单的例子
    
    # "引用命令替换"
    # 请注意,在 "$()" 中嵌套的引号无需转义。
    flag="$(some_command and its args "$@" 'quoted separately')"
    
    # "引用变量"
    echo "${flag}"
    
    # 使用数组与引用扩展以安全引用元素列表。
    declare -a FLAGS
    FLAGS=( --foo --bar='baz' )
    readonly FLAGS
    mybinary "${FLAGS[@]}"
    
    # 对内部整数变量不需要引用,这是可以的。
    if (( $# > 3 )); then
      echo "ppid=${PPID}"
    fi
    
    # "不引用文字整数"
    value=32
    # "引用命令替换",即使您期望是整数
    number="$(generate_number)"
    
    # "优先引用单词",不是必须的
    readonly USE_INTEGER='true'
    
    # "引用Shell元字符"
    echo '你好,陌生人,很高兴认识你。赚很多 $$$'
    echo "进程 $$:完成制作 $$$。"
    
    # "命令选项或路径名"
    # (假设 $1 在这里包含一个值)
    grep -li Hugo /dev/null "$1"
    
    # 较少简单例子
    # "引用变量,除非明确为假":ccs 可能为空
    git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}
    
    # 位置参数的预防措施:$1 可能未设置
    # 单引号保留正则表达式。
    grep -cP '([Ss]pecial||?characters*)$' ${1:+"$1"}
    
    # 对于传递参数,
    # "$@" 几乎每次都是正确的选择,而
    # $* 几乎每次都是错误的选择:
    #
    # * $* 和 $@ 会在空格上拆分,覆盖带有空格的参数
    # 并丢弃空字符串;
    # * "$@" 会保留参数,所以不提供参数将导致不传递参数;
    #   这在大多数情况下是传递参数时想要使用的方式。
    # * "$*" 扩展为一个参数,其中所有参数都由(通常)空格连接,
    #   所以不提供参数将导致传递一个空字符串。
    # (请查阅 `man bash` 以了解详细情况;- 注:指的是 `man bash` 中的相关内容)
    (set -- 1 "2 two" "3 three tres"; echo $#; set -- "$*"; echo "$#, $@")
    (set -- 1 "2 two" "3 three tres"; echo $#; set -- "$@"; echo "$#, $@")

    特性和错误

    ShellCheck

    ShellCheck 项目 可以识别您的Shell脚本中的常见错误和警告。无论脚本是大是小,都建议使用它。

    命令替换

    使用 $(command) 而不是反引号。

    嵌套的反引号需要用 转义内部的反引号。$(command) 格式在嵌套时不会改变,并且更容易阅读。

    示例:

    # 这是首选方式:
    var="$(command "$(command1)")"
    # 这不是:
    var="`command `command1``"

    测试,[ … ] 和 [[ … ]]

    [[ … ]] 优于 [ … ]test/usr/bin/[

    [[ … ]] 减少了错误,因为在 [[]] 之间不会发生路径名扩展或单词分割。此外,[[ … ]] 允许正则表达式匹配,而 [ … ] 不允许。

    # 这确保左边的字符串由 alnum 字符类中的字符组成,后跟字符串 name。
    # 请注意,RHS 在这里不应该被引用。
    if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
      echo "匹配"
    fi
    
    # 这匹配精确的模式 "f*"(在这种情况下不匹配)
    if [[ "filename" == "f*" ]]; then
      echo "匹配"
    fi
    # 这会导致 "参数过多" 错误,因为 f* 被扩展为当前目录的内容
    if [ "filename" == f* ]; then
      echo "匹配"
    fi

    要了解更多详细信息,请参阅 http://tiswww.case.edu/php/chet/bash/FAQ 中的 E14 部分。

    测试字符串

    在可能的情况下,请使用引号而不是填充字符。

    Bash 足够聪明,可以处理测试中的空字符串。因此,鉴于代码更容易阅读,对空/非空字符串或空字符串使用测试,而不是填充字符。

    # 使用这种方式:
    if [[ "${my_var}" == "some_string" ]]; then
      do_something
    fi
    
    # -z(字符串长度为零)和 -n(字符串长度不为零)优于测试空字符串
    if [[ -z "${my_var}" ]]; then
      do_something
    fi
    
    # 这是可以的(确保空边上的引号),但不是首选方式:
    if [[ "${my_var}" == "" ]]; then
      do_something
    fi
    # 不要这样:
    if [[ "${my_var}X" == "some_stringX" ]]; then
      do_something
    fi

    为避免对正在测试的内容产生困惑,明确地使用 -z-n

    # 使用这个
    if [[ -n "${my_var}" ]]; then
      do_something
    fi
    # 而不是这个
    if [[ "${my_var}" ]]; then
      do_something
    fi

    为了清晰起见,对于相等性使用 == 而不是 =,尽管两者都可以工作。前者鼓励使用 [[],而后者可能会与赋值混淆。然而,在 [[ … ]] 中使用 <> 时要小心,它执行词典比较。对于数字比较,请使用 (( … ))-lt-gt

    # 使用这个
    if [[ "${my_var}" == "val" ]]; then
      do_something
    fi
    
    if (( my_var > 3 )); then
      do_something
    fi
    
    if [[ "${my_var}" -gt 3 ]]; then
      do_something
    fi
    # 而不是这个
    if [[ "${my_var}" = "val" ]]; then
      do_something
    fi
    
    # 可能意外的词典比较。
    if [[ "${my_var}" > 3 ]]; then
      # 对于 4 为真,对于 22 为假。
      do_something
    fi

    文件名的通配符扩展

    在进行文件名的通配符扩展时,请使用显式路径。

    由于文件名可以以 - 开头,因此使用 ./* 而不是 * 扩展通配符更安全。

    # 下面是目录的内容:
    # -f  -r  somedir  somefile
    
    # 错误地强制删除了目录中的几乎所有内容
    psa@bilby$ rm -v *
    removed directory: `somedir'
    removed `somefile'
    # 相反:
    psa@bilby$ rm -v ./*
    removed `./-f'
    removed `./-r'
    rm: cannot remove `./somedir': Is a directory
    removed `./somefile'

    Eval

    应避免使用 eval

    eval 在用于变量赋值时会修改输入,并且可以设置变量而无法检查这些变量是什么。

    # 这会设置什么?
    # 它是否成功?部分或全部成功?
    eval $(set_my_variables)
    
    # 如果返回的值中有一个空格会发生什么?
    variable="$(eval some_function)"

    数组

    应使用 Bash 数组来存储元素列表,以避免引号问题。这特别适用于参数列表。不应使用数组来实现更复杂的数据结构(请参见上面的何时使用Shell)。

    数组存储有序的字符串集合,并且可以安全地扩展为命令或循环的单独元素。

    应避免使用单个字符串作为多个命令参数,因为这最终会导致作者使用 eval 或尝试在字符串中嵌套引号,这不会产生可靠或可读的结果,而且会导致不必要的复杂性。

    # 使用括号分配数组,可以使用 +=( … ) 追加。
    declare -a flags
    flags=(--foo --bar='baz')
    flags+=(--greeting="Hello ${name}")
    mybinary "${flags[@]}"
    # 不要使用字符串表示序列。
    flags='--foo --bar=baz'
    flags+=' --greeting="Hello world"'  # 这不会按预期工作。
    mybinary ${flags}
    # 命令扩展返回单个字符串,而不是数组。避免在数组分配中使用未引用的扩展,因为如果命令输出包含特殊字符或空格,它将无法正常工作。
    
    # 这将把列表输出扩展为字符串,然后执行特殊关键字扩展,然后进行空格分割。然后它才会变成一系列单词。ls 命令还可能根据用户的活动环境而改变行为!
    declare -a files=($(ls /directory))
    
    # get_arguments 将所有内容写入 STDOUT,然后经过上述的扩展过程,在变成一系列参数之前,经历了同样的过程。
    mybinary $(get_arguments)

    数组的优点

    • 使用数组可以存储事物的列表,而不会引起混淆的引号语义。相反,不使用数组会导致试图在字符串中嵌套引号。
    • 数组可以安全地存储任意字符串的序列/列表,包括包含空格的字符串。

    数组的缺点

    使用数组可能会增加脚本的复杂性。

    数组的决策

    应使用数组来安全地创建和传递列表。特别是在构建一组命令参数时,请使用数组来避免混淆的引号问题。使用带引号的扩展 – "${array[@]}" – 来访问数组。但是,如果需要更高级的数据操作,则应完全避免使用Shell脚本;请参见上面

    使用管道到While

    首选使用进程替代或 readarray 内置命令(bash4+),而不是使用管道到 while。管道会创建一个子shell,因此在管道内部修改的任何变量不会传播到父shell。

    管道到 while 中的隐式子shell可能会引入难以追踪的微妙错误。

    last_line='NULL'
    your_command | while read -r line; do
      if [[ -n "${line}" ]]; then
        last_line="${line}"
      fi
    done
    
    # 这将始终输出 'NULL'!
    echo "${last_line}"

    使用进程替代也会创建子shell。但是,它允许从子shell重定向到 while,而不需要将 while(或任何其他命令)放在子shell中。

    last_line='NULL'
    while read line; do
      if [[ -n "${line}" ]]; then
        last_line="${line}"
      fi
    done < <(your_command)
    
    # 这将输出 your_command 的最后一个非空行
    echo "${last_line}"

    或者,使用 readarray

    内置命令将文件读入数组,然后循环遍历数组的内容。请注意(与上面的原因相同),必须使用进程替代来代替管道,但优势是输入生成的位置位于循环之前,而不是之后。

    last_line='NULL'
    readarray -t lines < <(your_command)
    for line in "${lines[@]}"; do
      if [[ -n "${line}" ]]; then
        last_line="${line}"
      fi
    done
    echo "${last_line}"

    注意:在遍历输出时使用 for 循环(例如 for var in $(...))要小心,因为输出会按空格分割,而不是按行分割。有时您会知道这是安全的,因为输出不能包含任何意外的空格,但在这不明显或不会提高可读性的情况下(例如在 $(...) 内的长命令中),使用 while read 循环或 readarray 通常更安全和更清晰。

    算术

    始终使用 (( … ))$(( … )) 而不要使用 let$[ … ]expr

    绝对不要使用 $[ … ] 语法、expr 命令或 let 内置命令。

    <>[[ … ]] 表达式中不执行数值比较(它们执行的是词法比较;请参见测试字符串)。最好根本不要在数值比较中使用 [[ … ]],而应使用 (( … ))

    建议避免将 (( … )) 用作独立的语句,同时要注意其表达式是否评估为零,尤其是在启用了 set -e 的情况下。例如,set -e; i=0; (( i++ )) 将导致 shell 退出。

    # 用作文本的简单计算 - 注意在字符串内使用 $(( … ))。
    echo "$(( 2 + 2 )) is 4"
    
    # 在进行算术比较测试时
    if (( a < b )); then
      …
    fi
    
    # 将一些计算结果赋给变量。
    (( i = 10 * j + 400 ))
    # 这种形式不可移植且已过时
    i=$[2 * 10]
    
    # 尽管外观上看,'let' 不是声明性关键字之一,
    # 但未引用的赋值会受到单词分割的影响。
    # 为了简化起见,避免使用 'let',改用 (( … ))
    let i="2 + 2"
    
    # expr 实用程序是一个外部程序而不是 shell 内置命令。
    i=$( expr 4 + 4 )
    
    # 使用 expr 时引号可能会导致错误。
    i=$( expr 4 '*' 4 )

    尽管在风格上有考虑因素,但是 shell 的内置算术要比 expr 快得多。

    在使用变量时,不需要在 $(( … )) 中使用 ${var}(和 $var)形式。Shell 会自动为您查找 var,省略 ${…} 可以使代码更加简洁。尽管这与之前关于始终使用花括号的规则稍有不同,但这仅仅是一些建议。

    # 注意:在可能的情况下,请记得将变量声明为整数,并优先使用局部变量而不是全局变量。
    local -i hundred=$(( 10 * 10 ))
    declare -i five=$(( 10 / 2 ))
    
    # 将变量 "i" 增加三个。
    # 注意:
    # - 我们不写 ${i} 或 $i。
    # - 我们在 (( 前后加上空格。
    (( i += 3 ))
    
    # 要将变量 "i" 减少五个:
    (( i -= 5 ))
    
    # 进行一些复杂的计算。
    # 请注意,正常的算术运算符优先级会被遵循。
    hr=2
    min=5
    sec=30
    echo $(( hr * 3600 + min * 60 + sec )) # 预期输出 7530

    命名约定

    函数名

    小写,用下划线分隔单词。使用 :: 分隔库。函数名后必须加上括号。function 关键字是可选的,但在整个项目中必须保持一致使用。

    如果你只是写单个函数,使用小写并用下划线分隔单词。如果你在写一个包,用 :: 分隔包名。大括号必须与函数名在同一行(与 Google 的其他语言一样),函数名与括号之间不要留空格。

    # 单个函数
    my_func() {
      …
    }
    
    # 包的一部分
    mypackage::my_func() {
      …
    }

    当函数名后跟有“()”时,function 关键字是多余的,但它有助于快速识别函数。

    变量名

    与函数名相同。

    循环中的变量名应与要循环的任何变量具有相似的命名。

    for zone in "${zones[@]}"; do
      something_with "${zone}"
    done

    常量和环境变量名

    全部大写,用下划线分隔,放在文件顶部声明。

    常量和任何导出到环境的内容都应使用大写字母。

    # 常量
    readonly PATH_TO_FILES='/some/path'
    
    # 同时是常量和环境变量
    declare -xr ORACLE_SID='PROD'

    有些内容在首次设置时变成常量(例如,通过 getopts)。因此,可以在 getopts 中或基于条件设置常量,但之后应立即将其设置为只读。出于清晰起见,建议使用 readonlyexport,而不是等效的 declare 命令。

    VERBOSE='false'
    while getopts 'v' flag; do
      case "${flag}" in
        v) VERBOSE='true' ;;
      esac
    done
    readonly VERBOSE

    源文件名

    小写,如果需要,用下划线分隔单词。

    为了与 Google 的其他代码风格保持一致,使用小写的文件名,并用下划线分隔单词,如 maketemplatemake_template,而不是 make-template

    只读变量

    使用 readonlydeclare -r 确保它们是只读的。

    由于全局变量在 shell 中被广泛使用,因此在处理它们时捕获错误非常重要。当声明一个意味着只读的变量时,必须明确说明。

    zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
    if [[ -z "${zip_version}" ]]; then
      error_message
    else
      readonly zip_version
    fi

    使用局部变量

    使用 local 声明函数特定的变量。声明和赋值应位于不同的行。

    在声明局部变量时使用 local,以确保局部变量只能在函数及其子函数中访问。这样可以避免污染全局命名空间,并无意中设置可能在函数外部具有意义的变量。

    当赋值值由命令替换提供时,声明和赋值必须分开成两个语句,因为 local 内置命令不会传播命令替换的退出代码。

    my_func2() {
      local name="$1"
    
      # 分开的声明和赋值行:
      local my_var
      my_var="$(my_func)"
      (( $? == 0 )) || return
    
      …
    }
    my_func2() {
      # 不要这样做:
      # $? 总是为零,因为它包含 'local' 的退出代码,而不是 my_func 的退出代码
      local my_var="$(my_func)"
      (( $? == 0 )) || return
    
      …
    }

    函数位置

    将所有函数放在文件的常量下面。不要在函数之间隐藏可执行代码。这样做会使代码难以跟踪,并在调试时产生令人讨厌的意外。

    如果你有多个函数,将它们全部放在文件顶部附近。只有包含、set 语句和设置常量可以在声明函数之前执行。

    main

    对于至少包含一个其他函数的较长脚本,需要一个名为 main 的函数。

    为了方便找

    到程序的起始点,将主要代码放在一个名为 main 的函数中,作为最底部的函数。这与代码库的其余部分保持一致,同时还允许您将更多变量定义为 local(如果主要代码不是函数,则无法实现此功能)。文件中的最后一行非注释行应调用 main

    main "$@"

    当然,在只有线性流程的短脚本中,使用 main 是不必要的。

    调用命令

    检查返回值

    始终检查返回值并给出有信息的返回值。

    对于未使用管道的命令,使用 $? 或通过 if 语句直接检查,以保持简单。

    示例:

    if ! mv "${file_list[@]}" "${dest_dir}/"; then
      echo "无法将 ${file_list[*]} 移动到 ${dest_dir}" >&2
      exit 1
    fi
    
    # 或者
    mv "${file_list[@]}" "${dest_dir}/"
    if (( $? != 0 )); then
      echo "无法将 ${file_list[*]} 移动到 ${dest_dir}" >&2
      exit 1
    fi

    Bash 还有 PIPESTATUS 变量,允许检查管道的所有部分的返回代码。如果仅需要检查整个管道的成功或失败,那么以下内容是可以接受的:

    tar -cf - ./* | ( cd "${dir}" && tar -xf - )
    if (( PIPESTATUS[0] != 0 || PIPESTATUS[1] != 0 )); then
      echo "无法将文件打包到 ${dir}" >&2
    fi

    然而,由于一旦执行其他命令,PIPESTATUS 将被重写,如果需要根据管道中发生错误的位置采取不同的错误处理方式,你需要在运行命令后立即将 PIPESTATUS 赋值给另一个变量(不要忘记 [ 是一个命令,会清除 PIPESTATUS)。

    tar -cf - ./* | ( cd "${DIR}" && tar -xf - )
    return_codes=( "${PIPESTATUS[@]}" )
    if (( return_codes[0] != 0 )); then
      do_something
    fi
    if (( return_codes[1] != 0 )); then
      do_something_else
    fi

    内置命令 vs. 外部命令

    在选择调用内置命令和调用单独的进程之间,选择内置命令。

    我们更喜欢使用内置命令,例如 bash(1) 中的 参数扩展 函数,因为它更健壮和可移植(特别是与像 sed 这样的东西相比)。

    示例:

    # 更喜欢这种写法:
    addition=$(( X + Y ))
    substitution="${string/#foo/bar}"
    # 而不是这种写法:
    addition="$(expr "${X}" + "${Y}")"
    substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"

    结论

    理性思考并保持一致性

    请花几分钟阅读 C++ Guide 底部的 Parting Words 部分。

    修订版本 2.02

    赞(0)
    未经允许不得转载:拆东墙 » Google 团队 Shell 编码风格指南-中文版

    评论 抢沙发

    登录

    找回密码

    注册