Google Style Guides-Shell Style Guide

作者声明

这篇翻译文章对我来说是有点小挑战的。由于我英语实在非常烂,勉强能够看懂一些技术文档,能够猜出大概的含义。可是翻译对我来说算是一个挑战,看英文文档已经不是一天两天的事了,可是这个篇文章却是我的处女作,通读了这篇翻译后的文章,已经发现部分语句翻译的非常不好,可是我没有能力纠正好,加上时间上不同意,最后我还是厚着脸皮放到了博客上,假设有幸某位读者读到我这蹩脚的翻译。希望你能够在唾骂我的同一时候也给出正确的翻译让我能够修正.原文參见附录.

背景

使用什么shell?

Bash是同意的可运行文件里的的唯一一个shell脚本语言。

其可运行文件必须是以#!/bin/bash开头。使用set来设置shell的选项,以便你能够依照bash 来调用你的脚本。而不破坏它的功能.限制全部可运行的shell脚本为bash,为我们提供了所安装的全部机器上的一致shell语言。

唯一的例外是在种强制要求你编码为某种格式的地方,比如在Solaris SVR4的软件包中全部的脚本都要求是普通的Bourne shell.

什么时候使用shell?

shell应该仅仅被使用在一些小的使用工具或包装脚本中.尽管shell脚本不是一种开发语言,可是却用于整个Google公司中用于编写各种有用脚本,这样的风格引导很多其他的是认识它怎么去使用,而不是一个建议。它可用于广泛部署中.
一些准则:
* 假设你主要是调用一些其他的有用程序,和正在做一些相对较少的数据处理,那么shell是该任务能够接受的选择
* 假设你在乎性能,那么使用其他开发语言而不是shell
* 假设你发现你须要使用数组的地方超过变量赋值,你应该使用python
* 假设你写了一个脚本超过100行,你应该考虑使用python来取代。请记住脚本代码量会增长,尽早使用其他语言来重写你的脚本能够避免后期改动带来的大量的时间消耗

shell文件和解释器调用

文件扩展名

可运行文件应该没有扩展名(强烈推荐),或者使用.sh来作为扩展名,可是库文件必须有.sh作为扩展名,而且不可被运行.当你要运行一个运行文件的时候是没有必要知道这个可运行文件是什么语言编写的,因此shell是不须要扩展名,因此我们不希望给shell可运行文件加入扩展名.然而对于库文件来说。知道库是使用何种语言来编写的这是非常重要的,有的时候不同的语言可能会有相同的库,这就要求库文件必须能被区分。因此不同的语言通过给库文件加入预期的与语言相关的后缀名来进行识别.

SUID/SGID

SUID/SGID位在shell脚本中应该被禁止使用.
它们和shell在一起会有太多的安全问题,使其差点儿不能确保足以同意SUID/SGID,尽管bash确实让人非常难以SUID来运行。但它仍然有可能在一些平台上运行,这就是为什么我们要明白禁止使用它.假设你须要使用较高的权限来运行,使用sudo来取代.

环境

标准输出 vs 标准错误输出

全部的错误信息应该输入到标准错误输出中
这使的更easy从实际问题中分离出正常的状态.
以下这个函数是用于打印出错误信息以及其他状态信息的功能。值得推荐。

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

凝视

文件头凝视

在每个文件开头处加入一段描写叙述内容
每个文件必须有一个顶层凝视,这个凝视内容包括了一个简短的概述,一个copyright声明。另一些可选的作者信息
样例:

#!/bin/bash
#
# Perform hot backups of Oracle databases.

函数凝视

不论什么不能同一时候具备短小和功能显而易见的函数都必须又一段凝视。不论什么库中的函数,不管其长度大小和复杂性都必须要又凝视.
全部的函数凝视应该包括例如以下的内容:
* 函数的描写叙述信息
* 使用的和改动的全局变量
* 參数信息
* 返回值而不是最后一条命令的退出状态码
样例:

#!/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() {
  ...
}

详细实现细节相关凝视

对你的代码中比較棘手的,不easy理解的,不明显的,有趣的,或者是一些重要的部分加入凝视
遵循google的通用编码凝视的做法,不凝视一切。假设有一个复杂的算法。或者是你在做的一个与众不同的功能。在这些地方放置一个简单的凝视就可以.

TODO凝视

使用TODO来暂时凝视一段代码,表明这段代码是一个短期的解决方式。这段代码是够用的,但不完美.
这个凝视切割约定和Google C++ Guide一样.
全部的TODO类别的凝视,应该包括一个全部大写的字符串TODO,后面用括号包括您的username,冒号是可选的。这里最好把bug号,或者是ticket号放在TODO凝视后面
样例:

# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)

格式化

尽管你应该改动已有的文件来遵循以下风格,可是不论什么新编写的代码以下的风格是所必须的.

缩进

依照2个空格来缩进。不使用tab来缩进.
在两个语句块中使用空白行,来提高可读性,缩进是两个空格,不管你做什么,不要使用制表符。对于现有的文件,保留现有使用的缩进.

行的长度和字符串长度

一行的长度最大是80个字符.
假设你必须要写一个长于80个字符的字符串,那么你应该使用EOF或者嵌入一个新行,假设有一个文字字符串长度超过了80个字符。而且不能合理的切割文字字符串,可是强烈推荐你找到一种办法让它更短一点.
样例:

# 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."

多个管道

假设管道不能适应一行一个那么应该切割成每行一个. 
假设管道都适合在一行,那么就使用一行就可以.
假设不是,那么应该切割在每行一个管道,新的一行应该缩进2个空格,这条规则适用于那些通过使用”|”或者是一个逻辑运算符”||”和”&&”等组合起来的链式命令.
样例:

# All fits on one line
command1 | command2

# Long commands
command1 
  | command2 
  | command3 
  | command4

循环

; do; then和while for 以及if在同一行
shell中的循环有一点不同。可是我们遵循相同的原则。声明函数时使用括号,; then; do应该和if/for/while在同一行,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

Case语句

  • 通过2个空格来缩进
  • 对于一些简单的命令能够放在一行的,须要在右括号后面和;;号前面加入一个空格
  • 对于长的,有多个命令的。应该切割成多行,当中匹配项,对于匹配项的处理以及;;号各自在单独的行
    case和esac中匹配项的表达式应该都在同一个缩进级别,匹配项的处理也应该在另一个缩进级别.通常来说,没有必要给匹配项的表达式加入引號.匹配项的表达式不应该在前面加一个左括号。 避免使用;&;;&&等符号.
    样例:
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
don

变量表达式

依照优先顺序:保留你所发现的一致性的地方;将你的变量引起来,优先使用”var""var”,详情请看下文:
以下给出的约定目的仅仅是指导性的,假设把这些约定作为强制性规定似乎太有争议了.
这些约定依照以下的优先顺序列出:
1. 保留你发现的现有代码的一致性的地方
2. 将你的变量引起来,请看”引用”这个章节
3. 不要对单个字符的shell特殊变量或者是位置參数使用括号引用,除非强烈需求或者是为了避免深层次的困惑,优先使用括号引用其他不论什么变量

样例:

# Section of recommended cases.

# Preferred style for 'special' variables:
# 不要对单个字符的shell特殊变量或者是位置參数使用括号引用
echo "Positional: $1" "$5" "$3"
echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ $=$$ ..."

# Braces necessary:
# 避免深层次的困扰,$1和$10
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"

引用

  • 总是对包括变量的字符串,命令替换,空格或者是shell元字符进行引用。除非未引用的表达式是必须的.
  • 更倾向于引用包括单词的字符串,而不是命令选项和路径名称.
  • 不正确字面整数进行引用
  • 小心对于[[括号里的模式匹配使用引用规则(详细细节见特性和bug章节的Test, [ and [[部分)
  • 使用”@"使*

样例:

# 单引號引用。表明不须要变量或者命令替换
# 'Single' quotes indicate that no substitution is desired.
# 双引號引用。表明是须要变量或者命令替换的
# "Double" quotes indicate that substitution is required/tolerated.

# Simple examples
# "quote command substitutions"
# 双引號引用。引用内部进行变量替换
flag="$(some_command and its args "$@" 'quoted separately')"

# "quote variables"
echo "${flag}"

# "never quote literal integers"
# 不正确字面整数引用
value=32
# "quote command substitutions", even when you expect integers
number="$(generate_number)"

# "prefer quoting words", not compulsory
# 优先引用是单词的字符串,这不是必须的.
readonly USE_INTEGER='true'

# 引用shell元字符
# "quote shell meta characters"
echo 'Hello stranger, and well met. Earn lots of $$$'
echo "Process $$: Done making $$$."
# 命令选项和路径名不要引用
# "command options or path names"
# ($1 is assumed to contain a value here)
grep -li Hugo /dev/null "$1"

# Less simple examples
# "quote variables, unless proven false": ccs might be empty
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}

# Positional parameter precautions: $1 might be unset
# Single quotes leave regex as-is.
grep -cP '([Ss]pecial||?characters*)$' ${1:+"$1"}

# For passing on arguments,
# 对于參数传递来说,"$@"差点儿总是正确的,而$*总是错误的
# "$@" is right almost everytime, and
# $* is wrong almost everytime:
# 
# * $* and $@ will split on spaces, clobbering up arguments
#   that contain spaces and dropping empty strings;
# $@保持參数的原样,因此没有參数提供的时候,就不会有參数传递.
# * "$@" will retain arguments as-is, so no args
#   provided will result in no args being passed on;
#   This is in most cases what you want to use for passing
#   on arguments.
# $*将參数扩展成一个參数,全部的參数一般是依照空格连接,当没有提供參数的时候,将会导致一个空的字符串被传递.
# * "$*" expands to one argument, with all args joined
#   by (usually) spaces,
#   so no args provided will result in one empty string
#   being passed on.
# (Consult 'man bash' for the nit-grits ;-)

set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$*"; echo "$#, $@")
set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$@"; echo "$#, $@")

特性和bug

命令替换

使用(command)使(command)的这样的格式在嵌套的时候格式不须要改变,易读.
样例:

# This is preferred:
var="$(command "$(command1)")"

# This is not:
var="`command \`command1\``"

Test, [ and [[

[[... ]]条件測试要优于[...]条件測试
[[... ]] 降低了错误。没有路径名扩展,同意使用正則表達式和通配符,而[...]不行.
样例:

# 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

# 这里filename是要和精确字符串f*匹配,而不是和f*匹配模式进行匹配,模式是不能被引用的
# This matches the exact pattern "f*" (Does not match in this case)
if [[ "filename" == "f*" ]]; then
  echo "Match"
fi

# 由于路径名扩展导致f*扩展为当前文件夹下以f开头的文件和文件夹名,导致出现"too many argument"错误
# 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

字符串測试

使用引號而不是填充可能的字符
bash在处理測试一个空字符串是足够聪明的,因此鉴于让代码更易读,使用測试标志来測试字符串是否为空,而不是填充字符(见代码演示样例)
样例:

# 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
# 假设my_var替换为一个測试标志将会错误发生.
if [[ "${my_var}" ]]; then
  do_something
fi

文件名称的通配符扩展

当做文件名称通配符扩展的时候,使用显式路径.
作为文件名称能够使用-开头,这对于使用*取代./*有非常大的安全隐患.
样例:

# Here's the contents of the directory:
# 当前文件夹下又-f -r somedir somefile等文件和文件夹
# -f  -r  somedir  somefile

# 使用rm -v *将会扩展成rm -v -r -f somedir simefile,这将导致删除当前文件夹全部的文件和文件夹
# This deletes almost everything in the directory by force
psa@bilby$ rm -v *
removed directory: `somedir'
removed `somefile'

#相反假设你使用./*则不会,由于-r -f就不会变成rm的參数了
# As opposed to:
psa@bilby$ rm -v ./*
removed `./-f'
removed `./-r'
rm: cannot remove `./somedir': Is a directory
removed `./somefile'

Eval

eval命令应该被禁止运行.
eval用于给变量赋值的时候,能够设置变量,可是不能检查这些变量是什么
样例:

# 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)"

Pipes to While

使用进程替换,或for循环,要优于pipes to while(见下文),变量在while循环中改动的时候不会传播到上层作用域.由于循环命令是在子shell中运行的.pipe to while是在子shell中运行的,出现bug的时候非常难追踪.
样例:

#在while内部改动last_line将不会影响上层作用域中的last_line变量的
last_line='NULL'
your_command | while read line; do
  last_line="${line}"
done

# This will output 'NULL'
echo "${last_line}"

假设你确信输入不会包括空格和特殊字符的话。你能够使用for循环
样例:

total=0
# Only do this if there are no spaces in return values.
for value in $(command); do
  total+="${value}"
done

使用进程替换同意重定向输入。而且命令是在一个显示的子shell中运行的。而不是像bash给while loop创建的隐式子shell.
样例:

total=0
last_file=
while read count filename; do
  total+="${count}"
  last_file="${filename}"
done <<(your_command | uniq -c) #这里使用()显示子shell运行进程替换。而且同意重定向输入.
# This will output the second field of the last line of output from
# the command.
echo "Total = ${total}"
echo "Last one = ${last_file}"

使用while循环的时候,假设不须要传递一些复杂的结果给上层作用域,这通常须要一些复杂的解析。往往使用一些简单的命令配合awk这样的工具做起来更加简单。当你不想去改变上层作用域的全局变量的时候这样的方法非常有用.
样例:

# 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

名称约定

函数名

小写,下划线切割单词。通过::来切割库名,函数名后的括号是必须的。关键字function是可选的。可是必须在整个项目中保持一致.假设你写一个函数,使用小写,而且依照下划线切割单词,假设你写一个包,使用::切割包名,左花括号和函数名在同一行(和其他语言的google code style一样),函数名称和括号之间没有空格.

# Single function
my_func() {
  ...
}

# Part of a package
mypackage::my_func() {
  ...
}

在括号在函数名称之后出现,那么function这个关键字就是可选的了,其目的增强了对函数的高速识别.

变量名

和函数名规则相同
在循环中的变量名应该和要循环的变量名类似.
样例:

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

常量和环境变量名

全部的大写变量名依照下划线切割。声明在文件的開始处
常量和不论什么导出的环境变量应该大写.
样例:

# Constant
readonly PATH_TO_FILES='/some/path'

# Both constant and environment
# declare -r设置仅仅读变量。-x设置为环境变量
declare -xr ORACLE_SID='PROD' 

有些情况,变量须要在第一次被设置的时候,称为常量(比如 使用getopts这样的情况下),当然在getopts中基于某些条件来设置常量是非常正常的。可是你须要在设置完毕后马上让其变成仅仅读的,你须要注意的是。declare不能在函数内部操作全局变量。这个时候推荐受用readonly和export来取代.
样例:

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

源文件名称

小写而且依照下划线切割
这与其他的google code style是一致的,maketemplate 或 make_template而不是make-template.

仅仅读变量

使用readonly或declare -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"

  # 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)"  #local不会把my_func的退出码传递出去
  [[ $?

-eq 0 ]] || return ... }

函数位置

把全部的函数放在常量的以下。不要在函数之间隐藏可运行的代码.假设你要定义一个函数,请将这个函数放在文件的開始处。在函数声明之前仅仅能包括set语句,还有常量的设置.不要在函数之间隐藏可运行的语句。这样是的代码难以跟踪和调试。结果令人意外.

main

对于足够长的脚本来说。至少须要一个名为main的函数来调用其他的函数.
为了便于找到程序的開始。把主程序放在一个叫main的函数中。放在其他函数的以下,为了提供一致性你应该定义很多其他的变量为本地变量(假设主程序不是一个程序,那么不能这么做),文件里一句非凝视行应该是一个main函数的调用. 
样例:

main "$@"

显然,对于短脚本来说。程序都是线性的,main函数在这里是不必要的,所以不须要.

命令调用

检查返回值

总是应该检查返回值,给出返回值相关的信息.
对于一个未使用管道的命令,能够使用$?或者直接指向if语句来检查其返回值
样例:

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相同有PIPESTATUE变量同意检查管道命令全部部分的返回码,这仅仅用于检查整个管道运行成功与否.以下的样例是被接受的.
样例:

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的值给覆盖掉.)
样例:

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

内置命令 vs 外部命令

调用shell内置命令和调用一个单独的进程在两者这件做出选择,选择调用内置命令.
我更喜欢使用内置命令,比如函数參数扩展 (bash(1)),它更加健壮和便携.(尤其和像sed想比較而言)
样例:

# Prefer this:
addition=$((${X} + ${Y}))
substitution="${string/#foo/bar}"

# Instead of this:
addition="$(expr ${X} + ${Y})"
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"

总结

达成shell使用的共识和代码保持一致.
请花几分钟阅读下google code style C++ Guide的Parting words章节.

附录

Google Code Style
Shell Style Guide
Google Code Style部分中文版

原文地址:https://www.cnblogs.com/yfceshi/p/7096168.html