Google的Shell样式指南(俄语)

前言


使用哪个外壳


Bash可用于可执行文件Bash唯一Shell脚本语言。


脚本应以#!/bin/bash开头,并带有最少的标志集。 使用set来设置外壳程序选项,以便以bash <script_name>身份调用脚本不会违反其功能。


将所有shell脚本限制为bash,可以使我们在所有计算机上都安装一致的shell语言。


唯一的例外是如果您受编程条件的限制。 一个示例是Solaris SVR4软件包,该软件包需要对任何脚本使用常规的Bourne shell。


何时使用Shell


Shell仅应用于小型实用程序或简单的脚本包装程序。


尽管Shell脚本不是一种开发语言,但它可用于在Google各处编写各种实用程序。 该样式指南更多地是对其使用的认可,而不是建议广泛使用它。


一些建议:


  • 如果您经常调用其​​他实用程序并且进行相对较少的数据操作,那么Shell是执行此任务的可接受选择。
  • 如果性能很重要,请使用其他东西而不是外壳。
  • 如果发现除了分配${PIPESTATUS}之外,还需要使用数组,则应使用Python。
  • 如果编写的脚本长于100行,则可能应该用Python编写。 请记住,脚本正在增长。 尽早用另一种语言重写脚本,以避免以后进行耗时的重写。

Shell文件和解释器调用


文件扩展名


可执行文件不应具有扩展名(强烈推荐)或扩展名.sh 。 库必须具有扩展名.sh并且不能执行。


无需知道程序在执行过程中使用哪种语言编写,并且Shell不需要扩展名,因此我们不希望将其用于可执行文件。


但是,对于图书馆来说,知道用什么语言编写是很重要的,有时有必要使用不同的语言来建立相似的图书馆。 这使您可以使用具有相同目标的同名库文件,但是用不同语言编写的名称应相同,除了特定于语言的后缀之外。


SUID / SGID


Shell脚本禁止使用SUID和SGID。


安全问题太多,几乎无法提供足够的SUID / SGID保护。 尽管bash使SUID的启动复杂化,但是在某些平台上仍然可以实现,因此我们明确禁止使用它。


如果需要,请使用sudo进行增强的访问。


环境


STDOUT与STDERR


所有错误消息都应发送到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 

留言


文件头


在每个文件的开头加上其内容的描述。


每个文件都应有注释的标题,包括其内容的简短说明。 版权声明和作者信息是可选的。


一个例子:


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

实施意见


对代码的复杂,非显而易见,有趣或重要的部分进行注释。


假定这是在Google上注释代码的通常做法。 不要对所有内容发表评论。 如果算法复杂,或者您做的事情不寻常,请添加简短注释。


TODO评论


对临时的,短期的或相当不错的代码使用TODO注释,但并不完美。


这与C ++手册中的约定一致。


TODO注释应在大写字母中包含TODO字样,并在括号中加上您的名字。 冒号是可选的。 还最好在TODO元素旁边指示错误/故障单编号。


一个例子:


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

格式化


尽管您必须遵循正在编辑的文件中已经使用的样式,但是任何新代码都需要以下内容。


压痕


缩进2个空格。 没有标签。


在块之间使用空行以提高可读性。 缩进是两个空格。 无论您做什么,都不要使用选项卡。 对于现有文件,请遵循当前缩进。


字符串长度和值长度


最大行长为80个字符。


如果您需要编写多于80个字符的行,则应使用here document或内置的newline 。 可以允许使用长度超过80个字符且不能分隔的文字值,但是强烈建议您找到一种使它们更短的方法。


 #  'here document's cat <<END; I am an exceptionally long string. END #  newlines   long_string="I am an exceptionally long string." 

流水线


如果管道不适合在一条线上,则应将管道每条分开。


如果管线在一条线上,则应该在一条线上。


如果不是,则应进行划分,以使每个部分都换行,并在下一个部分缩进2个空格。 这是指使用“ |”组合的命令链 以及使用“ ||”的逻辑连接 和“ &&”。


 #      command1 | command2 #   command1 \ | command2 \ | command3 \ | command4 

周期数


地方; do ; do; then ; thenwhileforif在同一行。


Shell中的循环略有不同,但是在声明函数时,我们遵循与花括号相同的原理。 那是; then ; then; do ; do应该和/ 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 

案例陈述


  • 2个空格中的单独选项。
  • 单行选项在模板的右括号之后和;;之前需要一个空格。 。
  • 长命令或多命令选项应分为几行,分别包含模板,操作和;; 在单独的行上。

相应的表达式比caseesac退后一级。 多行操作还具有单独级别的缩进。 无需将表达式放在引号中。 表达式模式前不能带括号。 避免使用&;;;&表示法。


 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 done 

可变膨胀


按照优先顺序:观察已经使用的东西; 将变量括在引号中; 比"$var"更喜欢"${var}" ,但要注意使用的上下文。


这些是相当建议,因为该主题对于强制性法规颇有争议。 它们按优先级顺序列出。


  1. 使用与现有代码相同的样式。
  2. 将变量放在引号中,请参见下面的“报价”部分。
  3. 除非绝对必要,否则请勿将特定于外壳/位置参数的单个字符放在引号和花括号中,以免造成严重混淆。
    对于所有其他变量,最好使用花括号。


      #    #    '' : echo "Positional: $1" "$5" "$3" echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..." #   : echo "many parameters: ${10}" #    : # Output is "a0b0c0" set -- abc echo "${1}0${2}0${3}0" #     : echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}" while read f; do echo "file=${f}" done < <(ls -l /tmp) #    #   ,    , #     ,   shell echo a=$avar "b=$bvar" "PID=${$}" "${1}" #  : #    "${1}0${2}0${3}0",   "${10}${20}${30} set -- abc echo "$10$20$30" 

    行情


    • 对于包含变量,命令替换,空格或外壳程序元字符的值,请始终使用引号,直到需要安全地暴露不在引号中的值为止。
    • 对于“字”的值,最好使用引号(与命令参数或路径名相反)
    • 请勿引用整数。
    • 知道引号如何在[[用于匹配模式。
    • 如果没有特殊理由使用$*请使用"$@"


 # ''  ,     . # ""  ,   /. #   # "   " flag="$(some_command and its args "$@" 'quoted separately')" # "  " echo "${flag}" # "      " value=32 # "    ",      number="$(generate_number)" # "   ",    readonly USE_INTEGER='true' # "    - shell" echo 'Hello stranger, and well met. Earn lots of $$$' echo "Process $$: Done making \$\$\$." # "    " # ( ,  $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'  nit-grits ;-) set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$*"; echo "$#, $@") set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$@"; echo "$#, $@") 

功能和错误


命令替换


使用$(command)代替反引号。


嵌套的反引号要求使用\转义内部引号。 $ (command)格式不会根据嵌套而更改,并且更易于阅读。


一个例子:


 #  : var="$(command "$(command1)")" #  : var="`command \`command1\``" 

检查, [[[


[[ ... ]][test/usr/bin/[[[ ... ]]更可取。


[[ ... ]]减少了错误的可能性,因为[[]]之间没有路径解析或单词分隔,并且[[ ... ]]允许您使用正则表达式,而[ ... ]不允许。


 #  ,       #  `alnum`,     . #  ,       #  .  ,  # E14   https://tiswww.case.edu/php/chet/bash/FAQ if [[ "filename" =~ ^[[:alnum:]]+name ]]; then echo "Match" fi #     "f*" (    ) if [[ "filename" == "f*" ]]; then echo "Match" fi #    "too many arguments",   f*   #     if [ "filename" == f* ]; then echo "Match" fi 

检查值


尽可能使用引号而不是多余的字符。


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 #  ,    ,  ${my_var} #     . if [[ "${my_var}" ]]; then do_something fi 

文件名的替换表达式


为文件名创建通配符表达式时,请使用显式路径。


由于文件名可以以-字符开头,因此./*通配符表达式称为./*而不是*更为安全。


 #   : # -f -r somedir somefile #        force 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 $(set_my_variables) #  ,         ? variable="$(eval some_function)" 

While管


使用命令替换或for循环,而不是while管道。 在while更改的变量不会传播到父级,因为循环命令是在子shell中执行的。


while管道中的隐式子外壳可能使错误跟踪变得困难。


 last_line='NULL' your_command | while read line; do last_line="${line}" done #   'NULL' echo "${last_line}" 

如果您确定输入将不包含空格或特殊字符(通常这并不意味着用户输入),请使用for循环。


 total=0 #  ,       . for value in $(command); do total+="${value}" done 

使用命令替换可以重定向输出,但可以在显式子shell中执行命令,这与隐式子shell不同,后者为while创建bash。


 total=0 last_file= while read count filename; do total+="${count}" last_file="${filename}" done < <(your_command | uniq -c) #         # . echo "Total = ${total}" echo "Last one = ${last_file}" 

在不需要将复杂的结果传递给父外壳的地方使用while循环-当需要更复杂的“解析”时,这是典型的。 请记住,有时候使用awk之类的工具来解决简单的示例要容易得多。 当您特别不想修改父环境的变量时,它也很有用。


 #    awk: # 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上的其他语言相同),并且函数名称与括号之间不能有空格。


 #   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或基于条件来设置常量是很正常的,但是此后应立即以readonly进行设置。 请注意, declare不适用于函数内部的全局变量,因此建议使用readonlyexport


 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 exit code .


 my_func2() { local name="$1" #      : local my_var my_var="$(my_func)" || return #   : $?  exit code  'local',   my_func local my_var="$(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 #  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`,         .           ,   : ```bash 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 


shell , .


, bash, ( , sed ).


:


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

结论


.


请花几分钟阅读C ++手册底部的分词

Source: https://habr.com/ru/post/zh-CN413155/


All Articles