..

【翻译】谷歌shell编程规范

  1. 使用哪一种shell解释器。 bash应该是一唯一使用的脚本解释器,除此之外,你不应该使用其他的解释器的特性,除非你真的需要,或者是条件限制。确保你的脚本在移植到其他机器上也能够正常执行。bash脚本应该都包含一个shanbang,#!/bin/bash

  2. 什么时候应该是shell。 shell只应该被用来做一些很小的工具,或者是一些脚本的包装。 虽然shell不知一个正规的编程语言,但是google内部还是有很多地方在使用它。为了避免滥用,你应该准守如下规则:

  • 如果只是调用一些其他的工具,或者只是简单的处理输出,可以使用shell。
  • 如果你在意执行速度,那么不要使用shell,使用其他语言。
  • 如果你发现除了使用常规变量之外,你还需要使用数组,就像${PIPESTATUS},那么你应该使用python。
  • 如果你发现你的脚本长度超过了100行代码,那么你应该使用python来重写他,随着代码长度的增加,你应该尽快重构你的代码。
  1. 应该使用哪种后缀名。 我们强烈的建议你不要使用后缀名或者使用.sh作为后缀名。如果是最为依赖库,那么应该用.sh作为后缀名,并且不应该有执行权限。当我们执行一个脚本的时候,不需要知道他是哪种语言编写,并且shell也并没有shell也没有要求使用后缀名。然而,作为库代码,应该表明是使用哪种语言编写的,应该使用后缀来表明他的实现语言,比如.bash

  2. SUID和SGID。 shell脚本禁止使用SUID和SGID功能。 因为有太多的安全事故,所以必须禁止shell使用SUID和SGID,虽然在大部分平台上是难以开启这项功能的,但是某些平台上还是可以的,所以必须要禁止他,如果要获取权限,应该使用sudo

  3. 标准输出与标准错误输出。 所有的错误都应该被打印到标准错误输出stderr,这样更加方便正常信息和错误信息。建议使用一个单独的函数来输出错误信息:

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

if ! do_something; then
  err "Unable to do_something"
  exit "${E_DID_NOTHING}"
fi
  1. 文件头。 每个脚本的开头都必须有一段注释来描述这个脚本的内容。

  2. 函数注释。 不管函数的长度是多少,都应该添加注释。对于所有的库函数,都应该有注释描述。任何在使用库函数的时候,都应该能够通过注释的描述来使用这个库函数,或者是使用函数提供的--help选项(如果提供了的话)。函数的注释应该包含如下信息:

  • 函数的描述
  • 全局变量的使用和修改
  • 函数的参数说明
  • 函数的返回值和退出状态说明 例如:
#!/bin/bash
#
# Perform hot backups of Oracle databases.

export PATH='/usr/xpg4/bin:/usr/bin:/opt/csw/bin:/opt/goog/bin'

#######################################
# Cleanup files from the backup dir
# Globals:
#   BACKUP_DIR
#   ORACLE_SID
# Arguments:
#   None
# Returns:
#   None
#######################################
cleanup() {
  ...
}
  1. TODO说明。 对于临时的代码,或者是写的不好有待完善的代码,应该使用TODO说明。

  2. 代码格式。 当你添加新的代码,或者修改代码的时候,应该与之前的编码风格保持一致。

  3. 缩进。 使用两个空格作为缩进,不要使用tab。

  4. 字符串长度。 字符串的最大长度不要超过80个字符,如果超过了,请使用here docuemtn或者用shell的内置换行功能,强烈建议你找一个能够是字符串变短的方法。 例如:

# DO use 'here document's
cat <<END;
I am an exceptionally long
string.
END

# Embedded newlines are ok too
long_string="I am an exceptionally
  long string."
  1. 管道符号。 建议不讲所有管道符号写在同一行,如果有多个管道符号,应该写在多行,管道符号开头,并且使用两个空格作为缩颈。

例如:

# All fits on one line
command1 | command2

# Long commands
command1 \
  | command2 \
  | command3 \
  | command4
  1. 循环。 将;符号和do,then,for,if,while等关键词放在同一行。在shell中循环有一些不一样,但是我们应该遵循相关的准备:;thendo应该放在同一行,else放在单独一行。 例如:
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 [[ "$?" -ne 0 ]]; then
      error_message
    fi
  else
    mkdir -p "${dir}/${ORACLE_SID}"
    if [[ "$?" -ne 0 ]]; then
      error_message
    fi
  fi
done
  1. case语句。
  • 使用2个字符作为缩进。
  • 在每个case语句结束的最后,单独一样使用;;作为结束。
  • 比较长的语句应该被分成多行。 条件匹配应该在caseesac的基础上有一个缩进。满足条件的语句应该在当前shell中执行,不要使用一个子shell或者 &。 例如:
case "${expression}" in
  a)
    variable="..."
    some_command "${variable}" "${other_expr}" ...
    ;;
  absolute)
    actions="relative"
    another_command "${actions}" "${other_expr}" ...
    ;;
  *)
    error "Unexpected expression '${expression}'"
    ;;
esac

一些简单的语句可以和条件语句和;;放在同一行,但是一定要保准便于阅读。一定要语句结束之后,;;之前使用一个空格作为分隔符。

  1. 变量取值。 在保准和旧代码风格一致的前提条件下,优先使用"$var"而不是$var,除非必要,不要使用单引号和括号来引用变量。 例如:
# Section of recommended cases.

# Preferred style for 'special' variables:
echo "Positional: $1" "$5" "$3"
echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..."

# Braces necessary:
echo "many parameters: ${10}"

# Braces avoiding confusion:
# Output is "a0b0c0"
set -- a b c
echo "${1}0${2}0${3}0"

# Preferred style for other variables:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read f; do
  echo "file=${f}"
done < <(ls -l /tmp)

# Section of discouraged cases

# Unquoted vars, unbraced vars, brace-quoted single letter
# shell specials.
echo a=$avar "b=$bvar" "PID=${$}" "${1}"

# Confusing use: this is expanded as "${1}0${2}0${3}0",
# not "${10}${20}${30}
set -- a b c
echo "$10$20$30"

15. 引号。
* 永远要使用引号将字符串包裹起来。
* 不要使用引号包裹一个数字。

16. 子shell。
使用`$(command)`代码``command``,``commander``在嵌套的时候需要使用反引号来转义,而括号则不需要。

17. test。
推荐使用 `[[ ]]` , 而不是 `[ test str ]`
例如:
```shell
# This ensures the string on the left is made up of characters in the
# alnum character class followed by the string name.
# Note that the RHS should not be quoted here.
# For the gory details, see
# E14 at https://tiswww.case.edu/php/chet/bash/FAQ
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
  echo "Match"
fi

# This matches the exact pattern "f*" (Does not match in this case)
if [[ "filename" == "f*" ]]; then
  echo "Match"
fi

# This gives a "too many arguments" error as f* is expanded to the
# contents of the current directory
if [ "filename" == f* ]; then
  echo "Match"
fi
  1. 字符串判断。 使用双引号,而不是字符串过滤,shell对字符串支持非常不友好,应该尽量保准代码简单,便于阅读。 例如:
# Do this:
if [[ "${my_var}" = "some_string" ]]; then
  do_something
fi

# -z (string length is zero) and -n (string length is not zero) are
# preferred over testing for an empty string
if [[ -z "${my_var}" ]]; then
  do_something
fi

# This is OK (ensure quotes on the empty side), but not preferred:
if [[ "${my_var}" = "" ]]; then
  do_something
fi

# Not this:
if [[ "${my_var}X" = "some_stringX" ]]; then
  do_something
fi

为了避免混淆,应该使用-z或者-n

例如:

# Use this
if [[ -n "${my_var}" ]]; then
  do_something
fi

# Instead of this as errors can occur if ${my_var} expands to a test
# flag
if [[ "${my_var}" ]]; then
  do_something
fi
  1. 文件扩展名。 因为文件名可以使用-开头,所以应该使用./*来替代*
# Here's the contents of the directory:
# -f  -r  somedir  somefile

# This deletes almost everything in the directory by force
psa@bilby$ rm -v *
removed directory: `somedir'
removed `somefile'

# As opposed to:
psa@bilby$ rm -v ./*
removed `./-f'
removed `./-r'
rm: cannot remove `./somedir': Is a directory
removed `./somefile'
  1. Eval。 这个功能应该被禁止。

  2. 命名。 应该使用小写字符加下划线的组合。对于循环中的变量,应该和你当前正在迭代的变量尽量相似。

  3. 常量和环境变量。 常量和环境变量应该在文件的开头生命,应该大写字母加写划线的组合命名方式。一些一次读取以后就不在变化的变量,也应该用大写字母加下划线的方式来命令,并且使用readonly来申明。 例如:

VERBOSE='false'
while getopts 'v' flag; do
  case "${flag}" in
    v) VERBOSE='true' ;;
  esac
done
readonly VERBOSE
  1. 文件名。 文件名应该使用小写字母加下划线的组合方式。

  2. read-only变量。 使用readonly和declare来保证变量只读。