diff --git a/README.rst b/README.rst index 0296706..66a4323 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,8 @@ Google 开源项目风格指南 (中文版) #. `Google JSON 风格指南 `_ +#. `Google Shell 风格指南 `_ + 中文版项目采用 reStructuredText 纯文本标记语法, 并使用 Sphinx 生成 HTML / CHM / PDF 等文档格式. diff --git a/contents.rst b/contents.rst index f402138..b268cb2 100644 --- a/contents.rst +++ b/contents.rst @@ -11,3 +11,4 @@ google-cpp-styleguide/contents google-objc-styleguide/contents google-python-styleguide/contents + google-shell-styleguide/contents diff --git a/google-shell-styleguide/background.rst b/google-shell-styleguide/background.rst new file mode 100644 index 0000000..78c1135 --- /dev/null +++ b/google-shell-styleguide/background.rst @@ -0,0 +1,30 @@ +背景 +=============================== + +使用哪一种Shell +-------------------- + +.. tip:: + Bash是唯一被允许执行的shell脚本语言。 + +可执行文件必须以 ``#!/bin/bash`` 和最小数量的标志开始。请使用 ``set`` 来设置shell的选项,使得用 ``bash `` 调用你的脚本时不会破坏其功能。 + +限制所有的可执行shell脚本为bash使得我们安装在所有计算机中的shell语言保持一致性。 + +无论你是为什么而编码,对此唯一例外的是当你被迫时可以不这么做的。其中一个例子是Solaris SVR4包,编写任何脚本都需要用纯Bourne shell。 + + +什么时候使用Shell +-------------------- + +.. tip:: + Shell应该仅仅被用于小功能或者简单的包装脚本。 + +尽管Shell脚本不是一种开发语言,但在整个谷歌它被用于编写多种实用工具的脚本。这个风格指南更多的是认同它的使用,而不是一个建议,即它可被用于广泛部署。 + +以下是一些准则: + +* 如果你主要是在调用其他的工具并且做一些相对很小数据量的操作,那么使用shell来完成任务是一种可接受的选择。 +* 如果你在乎性能,那么请选择其他工具,而不是使用shell。 +* 如果你发现你需要使用数据而不是变量赋值(如 ``${PHPESTATUS}`` ),那么你应该使用Python脚本。 +* 如果你将要编写的脚本会超过100行,那么你可能应该使用Python来编写,而不是Shell。请记住,当脚本行数增加,尽早使用另外一种语言重写你的脚本,以避免之后花更多的时间来重写。 diff --git a/google-shell-styleguide/calling_commands.rst b/google-shell-styleguide/calling_commands.rst new file mode 100644 index 0000000..b0372cf --- /dev/null +++ b/google-shell-styleguide/calling_commands.rst @@ -0,0 +1,69 @@ +调用命令 +================================ + +检查返回值 +-------------------- + +.. tip:: + 总是检查返回值,并给出信息返回值。 + +对于非管道命令,使用 ``$?`` 或直接通过一个 ``if`` 语句来检查以保持其简洁。 + +例如: + +.. code-block:: shell + + if ! mv "${file_list}" "${dest_dir}/" ; then + echo "Unable to move ${file_list} to ${dest_dir}" >&2 + exit "${E_BAD_MOVE}" + fi + + # Or + mv "${file_list}" "${dest_dir}/" + if [[ "$?" -ne 0 ]]; then + echo "Unable to move ${file_list} to ${dest_dir}" >&2 + exit "${E_BAD_MOVE}" + fi + +Bash也有 ``PIPESTATUS`` 变量,允许检查从管道所有部分返回的代码。如果仅仅需要检查整个管道是成功还是失败,以下的方法是可以接受的: + +.. code-block:: shell + + tar -cf - ./* | ( cd "${dir}" && tar -xf - ) + if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; then + echo "Unable to tar files to ${dir}" >&2 + fi + +可是,只要你运行任何其他命令, ``PIPESTATUS`` 将会被覆盖。如果你需要基于管道中发生的错误执行不同的操作,那么你需要在运行命令后立即将 ``PIPESTATUS`` 赋值给另一个变量(别忘了 ``[`` 是一个会将 ``PIPESTATUS`` 擦除的命令)。 + +.. code-block:: shell + + tar -cf - ./* | ( cd "${DIR}" && tar -xf - ) + return_codes=(${PIPESTATUS[*]}) + if [[ "${return_codes[0]}" -ne 0 ]]; then + do_something + fi + if [[ "${return_codes[1]}" -ne 0 ]]; then + do_something_else + fi + +内建命令和外部命令 +-------------------- + +.. tip:: + 可以在调用shell内建命令和调用另外的程序之间选择,请选择内建命令。 + +我们更喜欢使用内建命令,如在 ``bash(1)`` 中参数扩展函数。因为它更强健和便携(尤其是跟像 ``sed`` 这样的命令比较) + +例如: + +.. code-block:: shell + + # Prefer this: + addition=$((${X} + ${Y})) + substitution="${string/#foo/bar}" + + # Instead of this: + addition="$(expr ${X} + ${Y})" + substitution="$(echo "${string}" | sed -e 's/^foo/bar/')" + diff --git a/google-shell-styleguide/comments.rst b/google-shell-styleguide/comments.rst new file mode 100644 index 0000000..a0bb345 --- /dev/null +++ b/google-shell-styleguide/comments.rst @@ -0,0 +1,82 @@ +注释 +================================ + +文件头 +-------------------- + +.. tip:: + 每个文件的开头是其文件内容的描述。 + +每个文件必须包含一个顶层注释,对其内容进行简要概述。版权声明和作者信息是可选的。 + +例如: + + .. code-block:: shell + + #!/bin/bash + # + # Perform hot backups of Oracle databases. + +功能注释 +-------------------- + +.. tip:: + 任何不是既明显又短的函数都必须被注释。任何库函数无论其长短和复杂性都必须被注释。 + +其他人通过阅读注释(和帮助信息,如果有的话)就能够学会如何使用你的程序或库函数,而不需要阅读代码。 + +所有的函数注释应该包含: + +* 函数的描述 +* 全局变量的使用和修改 +* 使用的参数说明 +* 返回值,而不是上一条命令运行后默认的退出状态 + +例如: + + .. code-block:: shell + + #!/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() { + ... + } + +实现部分的注释 +-------------------- + +.. tip:: + 注释你代码中含有技巧、不明显、有趣的或者重要的部分。 + +这部分遵循谷歌代码注释的通用做法。不要注释所有代码。如果有一个复杂的算法或者你正在做一些与众不同的,放一个简单的注释。 + +TODO注释 +-------------------- + +.. tip:: + 使用TODO注释临时的、短期解决方案的、或者足够好但不够完美的代码。 + +这与C++指南中的约定相一致。 + +TODOs应该包含全部大写的字符串TODO,接着是括号中你的用户名。冒号是可选的。最好在TODO条目之后加上 bug或者ticket 的序号。 + +例如: + + .. code-block:: shell + + # TODO(mrmonkey): Handle the unlikely edge cases (bug ####) + diff --git a/google-shell-styleguide/conclusion.rst b/google-shell-styleguide/conclusion.rst new file mode 100644 index 0000000..6aca6c4 --- /dev/null +++ b/google-shell-styleguide/conclusion.rst @@ -0,0 +1,7 @@ +结论 +================================ + +使用常识并保持一致。 + +请花几分钟阅读在C++风格指南底部的赠别部分。 + diff --git a/google-shell-styleguide/contents.rst b/google-shell-styleguide/contents.rst new file mode 100644 index 0000000..7ad045b --- /dev/null +++ b/google-shell-styleguide/contents.rst @@ -0,0 +1,24 @@ +.. 请确保至少包含基本的 `toctree` 指令 + +.. _shell_contents: + +=============================== +Shell 风格指南 - 内容目录 +=============================== + +.. contents:: + :backlinks: none + +.. toctree:: + :maxdepth: 3 + + index + background + shell_files_and_interpreter_invocation + environment + comments + formatting + features_and_bugs + naming_conventions + calling_commands + conclusion diff --git a/google-shell-styleguide/environment.rst b/google-shell-styleguide/environment.rst new file mode 100644 index 0000000..a0ee91b --- /dev/null +++ b/google-shell-styleguide/environment.rst @@ -0,0 +1,23 @@ +环境 +================================ + +STDOUT vs STDERR +-------------------- + +.. tip:: + 所有的错误信息都应该被导向STDERR。 + +这使得从实际问题中分离出正常状态变得更容易。 + +推荐使用类似如下函数,将错误信息和其他状态信息一起打印出来。 + +.. code-block:: shell + + 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 diff --git a/google-shell-styleguide/features_and_bugs.rst b/google-shell-styleguide/features_and_bugs.rst new file mode 100644 index 0000000..439a816 --- /dev/null +++ b/google-shell-styleguide/features_and_bugs.rst @@ -0,0 +1,194 @@ +特性及错误 +================================ + +命令替换 +-------------------- + +.. tip:: + 使用 ``$(command)`` 而不是反引号。 + +嵌套的反引号要求用反斜杠转义内部的反引号。而 ``$(command)`` 形式嵌套时不需要改变,而且更易于阅读。 + +例如: + +.. code-block:: shell + + # This is preferred: + var="$(command "$(command1)")" + + # This is not: + var="`command \`command1\``" + +test,[和[[ +-------------------- + +.. tip:: + 推荐使用 ``[[ ... ]]`` ,而不是 ``[`` , ``test`` , 和 ``/usr/bin/ [`` 。 + +因为在 ``[[`` 和 ``]]`` 之间不会有路径名称扩展或单词分割发生,所以使用 ``[[ ... ]]`` 能够减少错误。而且 ``[[ ... ]]`` 允许正则表达式匹配,而 ``[ ... ]`` 不允许。 + +.. code-block:: 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 http://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 + +测试字符串 +-------------------- + +.. tip:: + 尽可能使用引用,而不是过滤字符串。 + +Bash足以在测试中处理空字符串。所以,请使用空(非空)字符串测试,而不是过滤字符,使得代码更易于阅读。 + +.. code-block:: 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` + +.. code-block:: shell + + # 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 + +文件名的通配符扩展 +-------------------- + +.. tip:: + 当进行文件名的通配符扩展时,请使用明确的路径。 + +因为文件名可能以 ``-`` 开头,所以使用扩展通配符 ``./*`` 比 ``*`` 来得安全得多。 + +.. code-block:: shell + + # 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' + +Eval +-------------------- + +.. tip:: + 应该避免使用eval。 + +当用于给变量赋值时,Eval解析输入,并且能够设置变量,但无法检查这些变量是什么。 + +.. code-block:: shell + + # What does this set? + # Did it succeed? In part or whole? + eval $(set_my_variables) + + # What happens if one of the returned values has a space in it? + variable="$(eval some_function)" + +管道导向while循环 +-------------------- + +.. tip:: + 请使用过程替换或者for循环,而不是管道导向while循环。在while循环中被修改的变量是不能传递给父shell的,因为循环命令是在一个子shell中运行的。 + +管道导向while循环中的隐式子shell使得追踪bug变得很困难。 + +.. code-block:: shell + + last_line='NULL' + your_command | while read line; do + last_line="${line}" + done + + # This will output 'NULL' + echo "${last_line}" + +如果你确定输入中不包含空格或者特殊符号(通常意味着不是用户输入的),那么可以使用一个for循环。 + +.. code-block:: shell + + total=0 + # Only do this if there are no spaces in return values. + for value in $(command); do + total+="${value}" + done + +使用过程替换允许重定向输出,但是请将命令放入一个显式的子shell中,而不是bash为while循环创建的隐式子shell。 + +.. code-block:: shell + + total=0 + last_file= + while read count filename; do + total+="${count}" + last_file="${filename}" + done < <(your_command | uniq -c) + + # This will output the second field of the last line of output from + # the command. + echo "Total = ${total}" + echo "Last one = ${last_file}" + +当不需要传递复杂的结果给父shell时可以使用while循环。这通常需要一些更复杂的“解析”。请注意简单的例子使用如awk这类工具可能更容易完成。当你特别不希望改变父shell的范围变量时这可能也是有用的。 + +.. code-block:: shell + + # Trivial implementation of awk expression: + # awk '$3 == "nfs" { print $2 " maps to " $1 }' /proc/mounts + cat /proc/mounts | while read src dest type opts rest; do + if [[ ${type} == "nfs" ]]; then + echo "NFS ${dest} maps to ${src}" + fi + done diff --git a/google-shell-styleguide/formatting.rst b/google-shell-styleguide/formatting.rst new file mode 100644 index 0000000..733583b --- /dev/null +++ b/google-shell-styleguide/formatting.rst @@ -0,0 +1,235 @@ +格式 +================================ + +缩进 +-------------------- + +.. tip:: + 缩进两个空格,没有制表符。 + +在代码块之间请使用空行以提升可读性。缩进为两个空格。无论你做什么,请不要使用制表符。对于已有文件,保持已有的缩进格式。 + +行的长度和长字符串 +-------------------- + +.. tip:: + 行的最大长度为80个字符。 + +如果你必须写长度超过80个字符的字符串,如果可能的话,尽量使用here document或者嵌入的换行符。长度超过80个字符的文字串且不能被合理地分割,这是正常的。但强烈建议找到一个方法使其变短。 + +.. code-block:: shell + + # DO use 'here document's + cat <`_ v1.26 + +:项目主页: + + - `Google Style Guide `_ + - `Google 开源项目风格指南 - 中文版 `_ + diff --git a/google-shell-styleguide/naming_conventions.rst b/google-shell-styleguide/naming_conventions.rst new file mode 100644 index 0000000..c500a1d --- /dev/null +++ b/google-shell-styleguide/naming_conventions.rst @@ -0,0 +1,140 @@ +命名转换 +================================ + +函数名 +-------------------- + +.. tip:: + 使用小写字母,并用下划线分隔单词。使用双冒号 ``::`` 分隔库。函数名之后必须有圆括号。关键词 ``function`` 是可选的,但必须在一个项目中保持一致。 + +如果你正在写单个函数,请用小写字母来命名,并用下划线分隔单词。如果你正在写一个包,使用双冒号 ``::`` 来分隔包名。大括号必须和函数名位于同一行(就像在Google的其他语言一样),并且函数名和圆括号之间没有空格。 + +.. code-block:: shell + + # Single function + my_func() { + ... + } + + # Part of a package + mypackage::my_func() { + ... + } + +当函数名后存在 ``()`` 时,关键词 ``function`` 是多余的。但是其促进了函数的快速辨识。 + +变量名 +-------------------- + +.. tip:: + 如函数名。 + +循环的变量名应该和循环的任何变量同样命名。 + +.. code-block:: shell + + for zone in ${zones}; do + something_with "${zone}" + done + +常量和环境变量名 +-------------------- + +.. tip:: + 全部大写,用下划线分隔,声明在文件的顶部。 + +常量和任何导出到环境中的都应该大写。 + +.. code-block:: shell + + # Constant + readonly PATH_TO_FILES='/some/path' + + # Both constant and environment + declare -xr ORACLE_SID='PROD' + +第一次设置时有一些就变成了常量(例如,通过getopts)。因此,可以在getopts中或基于条件来设定常量,但之后应该立即设置其为只读。值得注意的是,在函数中 ``declare`` 不会对全局变量进行操作。所以推荐使用 ``readonly`` 和 ``export`` 来代替。 + +.. code-block:: shell + + VERBOSE='false' + while getopts 'v' flag; do + case "${flag}" in + v) VERBOSE='true' ;; + esac + done + readonly VERBOSE + +源文件名 +-------------------- + +.. tip:: + 小写,如果需要的话使用下划线分隔单词。 + +这是为了和在Google中的其他代码风格保持一致: ``maketemplate`` 或者 ``make_template`` ,而不是 ``make-template`` 。 + +只读变量 +-------------------- + +.. tip:: + 使用 ``readonly`` 或者 ``declare -r`` 来确保变量只读。 + +因为全局变量在shell中广泛使用,所以在使用它们的过程中捕获错误是很重要的。当你声明了一个变量,希望其只读,那么请明确指出。 + +.. code-block:: shell + + zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)" + if [[ -z "${zip_version}" ]]; then + error_message + else + readonly zip_version + fi + +使用本地变量 +-------------------- + +.. tip:: + 使用 ``local`` 声明特定功能的变量。声明和赋值应该在不同行。 + +使用 ``local`` 来声明局部变量以确保其只在函数内部和子函数中可见。这避免了污染全局命名空间和不经意间设置可能具有函数之外重要性的变量。 + +当赋值的值由命令替换提供时,声明和赋值必须分开。因为内建的 ``local`` 不会从命令替换中传递退出码。 + +.. code-block:: shell + + my_func2() { + local name="$1" + + # Separate lines for declaration and assignment: + local my_var + my_var="$(my_func)" || return + + # DO NOT do this: $? contains the exit code of 'local', not my_func + local my_var="$(my_func)" + [[ $? -eq 0 ]] || return + + ... + } + +函数位置 +-------------------- + +.. tip:: + 将文件中所有的函数一起放在常量下面。不要在函数之间隐藏可执行代码。 + +如果你有函数,请将他们一起放在文件头部。只有includes, ``set`` 声明和常量设置可能在函数声明之前完成。不要在函数之间隐藏可执行代码。如果那样做,会使得代码在调试时难以跟踪并出现意想不到的讨厌结果。 + +主函数main +-------------------- + +.. tip:: + 对于包含至少一个其他函数的足够长的脚本,需要称为 ``main`` 的函数。 + +为了方便查找程序的开始,将主程序放入一个称为 ``main`` 的函数,作为最下面的函数。这使其和代码库的其余部分保持一致性,同时允许你定义更多变量为局部变量(如果主代码不是一个函数就不能这么做)。文件中最后的非注释行应该是对 ``main`` 函数的调用。 + +.. code-block:: shell + + main "$@" + +显然,对于仅仅是线性流的短脚本, ``main`` 是矫枉过正,因此是不需要的。 + diff --git a/google-shell-styleguide/shell_files_and_interpreter_invocation.rst b/google-shell-styleguide/shell_files_and_interpreter_invocation.rst new file mode 100644 index 0000000..24efeb2 --- /dev/null +++ b/google-shell-styleguide/shell_files_and_interpreter_invocation.rst @@ -0,0 +1,23 @@ +Shell文件和解释器调用 +=============================== + +文件扩展名 +-------------------- + +.. tip:: + 可执行文件应该没有扩展名(强烈建议)或者使用.sh扩展名。库文件必须使用.sh作为扩展名,而且应该是不可执行的。 + +当执行一个程序时,并不需要知道它是用什么语言编写的。而且shell脚本也不要求有扩展名。所以我们更喜欢可执行文件没有扩展名。 + +然而,对于库文件,知道其用什么语言编写的是很重要的,有时候会需要使用不同语言编写的相似的库文件。使用.sh这样特定语言后缀作为扩展名,就使得用不同语言编写的具有相同功能的库文件可以采用一样的名称。 + + +SUID / SGID +-------------------- + +.. tip:: + SUID(Set User ID)和SGID(Set Group ID)在shell脚本中是被禁止的。 + +shell存在太多的安全问题,以致于如果允许SUID/SGID会使得shell几乎不可能足够安全。虽然bash使得运行SUID非常困难,但在某些平台上仍然有可能运行,这就是为什么我们明确提出要禁止它。 + +如果你需要较高权限的访问请使用 ``sudo`` 。