暗无天日

=============>DarkSun的个人博客

Bash_Style_Guide_and_Coding_Standard

通常来说, 脚本编程有快速编写,难以理解,即完即扔的特点,因此也无需有任何质量上的要求. 但是这一观点忽略了一个事实,那就是在很多领域,脚本的生命周期都很长: 系统管理, 操作系统配置, 软件安装, 自动化的用户任务等等. 很明显,这些脚本都需要维护,扩展并且文档化. 因此,脚本语言编程也应该满足生产语言编程一样的要求(能满足目的, 正确的实现方法, 能满足各种需求, 足够健壮, 易于维护)和标准. 一个程序要维护,就需要让它的结构和功能能够很容易地被他人理解, 唯有这样别人才能在合理的时间内最其作出合适的修改. 而如何满足这些要求很大程度上取决于所使用的编程风格. 本指南的主要目的就是让你能写出易懂,可维护的代码.

Length of line

一行不能超过80个字符长度(包括注释). 这样在搜索的时候就不用左右拖动,而且在打印文件时也可以直接打印到平常宽度的纸上而不会导致代码行被截断或者换行. 为此,可能需要将一行命令或文本拆成多行来写.

Indentation

程序结构的缩进必须能体现程序逻辑层次. 一步缩进的步进通常与所用编辑器的制表符的步进一致. 通常为2,4或者8.

Comments

Introductory comments in files

每个文件都需要有一份介绍性的说明,这份说明提供文件名称及其内容的相关信息.

#!/bin/bash
#===================================================================================
#
#         FILE:  stale-links.sh
#
#        USAGE:  stale-links.sh [-d] [-l] [-oD logfile] [-h] [starting directories]
#
#  DESCRIPTION:  List and/or delete all stale links in directory trees.
#                The default starting directory is the current directory.
#                Don’t descend directories on other filesystems.
#
#      OPTIONS:  see function ’usage’ below
# REQUIREMENTS:  ---
#         BUGS:  ---
#        NOTES:  ---
#       AUTHOR:  Dr.-Ing. Fritz Mehner (fgm), mehner.fritz@fh-swf.de
#      COMPANY:  FH Südwestfalen, Iserlohn
#      VERSION:  1.3
#      CREATED:  12.05.2002 - 12:36:50
#     REVISION:  20.09.2004
#===================================================================================

如有必要,也可以加上其他信息(比如. 版权,项目分配等信息)

Line end comments

单行注释与代码在统一行. 在注释符 # 后应该跟一个空格,这样可以很容易地区分出单词的头部.

found=0   # count links found
deleted=0 # count links deleted

Section comments

如果多行逻辑上构成一个段,那么就应该为段添上段注释.

#----------------------------------------------------------------------
#  delete links, if demanded write logfile
#----------------------------------------------------------------------
if
    [ "$action" == ’d’ ] ;
then
    rm --force "$file" && ((deleted++))
    echo "removed link :  ’$file’"
    [ "$logfile" != "" ] && echo "$file" >> "$logfile"
fi

段注释应该与后面代码的缩进位置对其.

Function comments

每个函数都应该有一个介绍性的注释. 其中包括函数名,函数的简单说明以及函数参数的说明(如果有的话). 如果后续对该函数有所修改的话,应该把修改人和修改日期也加进来.

#===  FUNCTION  ================================================================
#         NAME:  usage
#  DESCRIPTION:  Display usage information for this script.
# PARAMETER  1:  ---
#===============================================================================

Commenting style

注释风格应该遵循如下原则:

简短、简洁、准确

注释的目的是便于理解. 只有在特殊情况下才会用注释来描述代码结构或其中用到的一些小技巧:

注释应该描述代码的目的.

例如,下面这段注释就没啥用,它只是重复前面这一行的代码而已:

[ "$logfile" != "" ] && $(> "$logfile") # variable $logfile empty ?

而下面这句注释则简洁第描述了代码的意图:

[ "$logfile" != "" ] && $(> "$logfile") # empty an existing logfile

Variables and constants

Use of variables

变量名应该是有意义的,需要能够望名知意(比如inputfile). 变量名中前31个字符不能重复. 如果名字特别的长,可以用下划线分割命名中的各个部分以提高可读性.

若变量名确实无法做到望名知意, 那么第一次使用该变量时必须在注释中写清楚该变量的意义和使用方法.

Use of constants

Principally, the following applies for all programming languages: No constants must be included in the program text ! In particular numeral constants do not have another immediate meaning apart from their value. The meaning of the value will only become clear in the specific text context. In case of value changes of multiple occurring constants an automatic replacement in the editor is not possible, because the value might have been used in different meanings. Such program texts therefore are difficult to maintain. For the handling of constants - and of course also constant texts (such as file names) - the following recommendations apply:

  • Global constants and texts.

    Global constants and texts (e.g. file names, replacement values for call parameters and the like) are collected in a separate section at the beginning of the script and commented individually, if the number is not too high.

    startdirs=${@:-.} # default start directory: current directory
    action=${action:-l} # default action is -l (list)
    
  • 大段文本. 引用大段的文本 (例如. 描述性的文字, 对调用选项的说明文档) 时可以使用 here documents.

    cat
    <<- EOT
    List and/or delete all stale links in directory trees.
    usage : $0 [-d] [-oD logfile] [-l] [-h] [starting directories]
    -d    delete stale links
    -l    list stale links (default)
    -o    write stale links found into logfile
    -D    delete stale links listed in logfile
    -h    display this message
    EOT
    

Success verification

Command line options

若对参数个数有要求,那么就应该在脚本中对参数个数进行校验. 当调用参数有误时,脚本可以终止运行并返回错误信息或/并说明需要调用的参数是什么.

参数的值也应该校验有效性. 例如,当传递一个文件为参数值时,在读该文件之前应该先测试一下文件是否存在且具有可读权限(例如. 使用 [ -r $inputfile ] 来进行测试).

Variables, commands and functions

变量在使用前必须先为之设置一个有意义的初始值. 像这样:

[ -e "$1" ] && expand --tabs=$number "$1" > "$1.expand"

它会先检查参数 $1 所代表的文件是否存在. 逻辑表达式会在左子句就能确定整个表达式结果的情况下终止对右子句的运行(即所谓的短路执行),因此当前一个条件为假时,就不会进行进一步的处理. 最后命令的返回值会存储在变量 $? 中,可以将之运用于后续的处理控制中:

mkdir "$new_directory"  2> /dev/null
if
    [ $? -ne 0 ]
then
    ...
fi

在本例中,若无法创建目录,则 mkdir 的返回值就不会是0. 另外,变量 $? 还可以用于检查函数的返回值.

Execution and summary reports

交互式应用的脚本应该要显示一份汇总报告. 从这份报告中可以判断脚本是否运行正常,还能用于检查结果的可信度,例如.

mn4:~/bin # ./stale-links -o stale-links.log /opt
... searching stale links ...
1. stale link:  ’/opt/dir link 23’
2. stale link:  ’/opt/file link 71’
3. stale link:  ’/opt/file link 7’
      stale links   found : 3
      stale links deleted : 0
      logfile: ’stale-links.log’

有关细节的执行报告存在日志文件中. 这些日志文件中的内容也需要有助于诊断失败的原因.

Files

  • 文件名

    主文件名应该是有意义的. 文件扩展名则应该尽可能的反应出文件的内容(比如.dat , .log , .lst , .tmp 等等.).

  • 临时文件

    临时文件一般用于存放中间结果,并且这些文件一般统一放在 tmp 目录中,用完即删. 可以使用 mktemp 来生成随机的文件名(参见 man 1 mktemp):

    #-------------------------------------------------------------------------------
    #  Cleanup temporary file in case of keyboard interrupt or termination signal.
    #-------------------------------------------------------------------------------
    function cleanup_temp {
        [ -e $tmpfile ] && rm --force $tmpfile
        exit 0
    }
    
    trap cleanup_temp  SIGHUP SIGINT SIGPIPE SIGTERM
    
    tmpfile=$(mktemp) || {echo "$0: creation of temporary file failed!"; exit 1; }
    
    # ... use tmpfile ...
    
    rm --force $tmpfile
    

    若触发了 trap 语句中指定的其中一种信号,在终止脚本执行的同时,还会调用函数 cleanup_temp. 然后该函数就会清除临时文件了. 只有当脚本被 SIGKILL 信号终止运行的情况下才会保留临时文件,因为该信号无法被捕获.

  • 备份文件

    如果需要保留多个旧的文件副本,那么建议使用时间来进行区分:

    timestamp=$(date +"%Y%m%d-%H%M%S") # generate timestamp : YYYYMMDD-hhmmss
    mv logfile logfile.$timestamp
    

    文件 logfile 就会被重命名为类似 logfile.20041210-173116 这样. 文件名中的时间和日期是以逆序的形式来组织的(The components of date and time are organized in reversed order??什么意思). 以这种方式命名的文件在目录中的排列会按照时间的自然顺序来排列的.

  • 中间结果

    通过使用 tee 命令,可以将中间结果同时写入文件和标准输出中. 这样一来,你就可以使用中间结果来控制处理流程或者用于测试脚本:

    echo $output_string | tee --append  $TMPFILE
    

Command line options

  • 调用外部程序

    在脚本中调用系统程序时,应该尽可能的使用 GNU风格的命令行参数(参数的完整形式). GNU风格的参数一般都很能表达出参数的意义,因此有助于理解脚本做的事情. 在下面的useradd命令中,我们使用了 -c , -p-m 的完整形式:

    useradd --comment "$full_name" \
            --password "$encrypted_password"  \
            --create-home \
            $loginname
    

    通过断行符(行末尾的 \ 字符) 可以避免写出太长的一行代码. 参数前的缩进则增加了可读性.

  • 自己脚本的命令行参数

    自己设计参数字母时 (参数的缩写形式) 应该尽可能选择直观的或者普遍使用的字母 (例如. -f 用于指定文件, 或者 -d, 用于是否输出额外的信息(debug)). 对于参数的完整形式, 建议参照 [[http://www.gnu.org/prep/standards.html][GNU Coding Standards]]

Use of Shell Builtin Commands

尽可能使用shell内建命令而不是外部程序. 因为每次调用 sed , awk , cut 等外部命令都会产生一个新的进程. 若在循环语句中反复调用会显著地增加运行所需要的时间. 在下面的例子中,我们使用shell参数扩展机制来获取文件路径中的文件名和目录路径:

for
    pathname in $(find $search - type f -name "*" -print)
do
    basename=${pathname##*/} # replaces basename(1)
    dirname=${pathname%/*} # replaces dirname(1)
    ...
done

可以使用比较符 =~ 来对字符串进行模式匹配.

metacharacter=’[~&|]’
if [[ "$pathname" =~ $metacharacter ]]
then
    # treat metacharacter
fi

这种模式匹配兼容POSIX regular expressions (regex(7)).

Portability

使用 dash-shell (Debian Almquist Shell) 通常能够保证脚本满足 POSIX 兼容性([POS13]). 你也可以在 [Bas13] 中找到那些不可移植的结构及其对应的可移植的替代.

SUID/SGID-Scripts

shell脚本受到用户输入,进程环境,初始化文件,所使用的系统工具等等各方面的影响. Shell语言不适合用来写与安全有关的脚本,因为上面所有这些因素 (当然还有其他没列出来的) 都可能用于攻击你的系统. Utilities may be vulnerable themselves. 要小心运行那些带 SUID/SGID 标志位的脚本 [GSS03 , Whe03]. 下面仅列几条特别重要的预防措施:

  • Execute the script from a directory where it can not be changed unauthorized.
  • 检查环境变量 BASH_ENV 是否为空.
  • 设置 umask 为 077.
  • 重置环境变量 PATH and IFS 为一个安全的值.
  • 切换到一个安全的工作目录并验证是否成功切换过去了.
  • 使用绝对路径来调用系统工具及数据文件.
  • 每次调用系统工具都要检查返回码.
  • 使用 -- 来标识选项参数的结束.
  • 将所有的命令行参数都引用起来 (例如. "$1").
  • 检查用户输入中是否包含了shell metacharacters(特殊意义的字符)或者其他非法字符.
  • 检查用户提供的路径名 (绝对路径/相对路径).
  • 开启shell选项 noclobber 以防止覆盖已有文件.
  • 使用 mktemp 创建临时文件(参见 section 6)

Testing

Syntax check

使用 Bash 加上 -n 参数来执行脚本, 则只会读取脚本命令而不会真正去执行他:

bash  -n  remove_ps.sh

这种方法可以用于进行语法检查. 不过这种方法只能检查出致命错误. 比如它无法检测出关键字不全的情况 (比如 echo 错写成了 cho), 因为很可能有一个同名的程序或者函数可以被调用.

Test scope

在开发阶段就有必要准备好测试环境,包括准备测试文件或者测试数据. 不过这些测试数据不需要太过复杂. 这会增加脚本开发的速度,并且减少无意中改动重要数据的风险.

Use of echo

会影响系统变更的命令(比如删除或者重命名文件)在测试时,应该先用 echo 将之输出并进行检查. 这点很重要,尤其当处理对象中包含通配符或者递归目录进行处理的时候.

下面代码

for file in *.sh
do
    rm  "$file"
done

会立即删除目录中所有以 .sh 为后缀名的文件.

在删除命令前加上 echo 命令, 就会输出要执行的删除命令了.

echo "rm  \"$file\""

在确定无误后,再把 echo 删除.

Testing using Bash options

Command line option set -o Option Meaning
-n noexec Commands are not executed, only syntax check (see 11.1)
-v verbose Outputs the lines of a script before execution.
-x xtracd Outputs the lines of a script after replacements.

下面几行代码

TMPFILE=$( mktemp /tmp/example.XXXXXXXXXX ) || exit 1
echo "program output" >> $TMPFILE
rm --force $TMPFILE

若执行时加上 -xv 选项

bash -xv ./tempfile.sh

会有如下输出:

TMPFILE=$( mktemp /tmp/example.XXXXXXXXXX ) || exit 1
mktemp /tmp/example.XXXXXXXXXX
++ mktemp /tmp/example.XXXXXXXXXX
+ TMPFILE=/tmp/example.AVkuGd6796
echo "program output" >> $TMPFILE
+ echo ’program output’
rm --force $TMPFILE
+ rm --force /tmp/example.AVkuGd6796

+ 开头的行是由 -x 选项输出的. 加号的数量表示了变量替换的层级.

这些选项可以在脚本的任意位置进行重新设置:

set -o xtrace # --- xtrace on ---
for
    file in $list
do
    rm  "$file"
done
set +o xtrace # --- xtrace off ---

The use of PS4

上一小节中, 由 -x 生成的前缀是由变量 PS4 所决定的,默认值为 ’+’. 在需要的时候,会重复字符串中的第一个字符以标识调用深度. 可以通过修改变量 PS4 的值来获得更多的信息.

例如:

# PS4 : position, line number, function name
# The following line avoids error messages due to an unset FUNCNAME[0] :
set +o nounset
# Treat unset variables not as an error
PS4='+|${BASH_SOURCE##*/} ${LINENO}${FUNCNAME[0]:+ ${FUNCNAME[0]}}|  '

下面是一个输出的例子:

+| test.sh 41| for n in ’{1..4}’
+| test.sh 42|  function1
+| test.sh 30 function1| echo ’-- in function1 --’
-- in function1 --
+| test.sh 31 function1|  function2
+| test.sh 37 function2| echo ’-- in function2 --’
-- in function2 --
+| test.sh 32 function1| echo ’-- in function1 again --’
-- in function1 again --

提示符 PS4 还能用来输出时间戳.

# PS4 : timestamp; the current time in 24-hour HH:MM:SS format
PS4=’+[\t]  ’
# PS4 : timestamp; ’seconds.nanoseconds’ since 1970-01-01 00:00:00 UT
PS4=’+[$(date "+%s.%N")]  ’

Testing by means of trap

Bash 提供了两种仿真信号,可以为这两种仿真信号设置各自的处理行为.

Pseudo signal Trigger
DEBUG The shell has executed a command.
EXIT The shell terminates the script.

图1 演示了 trap 命令捕获这两个仿真信号的例子. 图二为输出.

Figure 1: Example for the use of pseudo signals and trap

#===  FUNCTION  ================================================================
# NAME:  dbgtrap
#  DESCRIPTION:  monitor the variable ’act_dir’
#===============================================================================
function dbgtrap ()
{
    echo "act_dir = \"$act_dir\""
}    # ----------  end of function dbgtrap  ----------
#-----------------------------------------------------------------------
#  traps
#-----------------------------------------------------------------------
trap ’ echo "On exit : act_dir = \"$act_dir\""’  EXIT
trap dbgtrap DEBUG
#-----------------------------------------------------------------------
#  monitoring ...
#-----------------------------------------------------------------------
act_dir=$(pwd)
cd ..
act_dir=$(pwd)
cd $HOME

Figure 2: Output of script in figure 1

act_dir = ""
act_dir = "/home/mehner"
act_dir = "/home/mehner"
act_dir = "/home"
act_dir = "/home"
act_dir = "/home"
On exit : act_dir = "/home"

The debugger bashdb

调试器 bashdb 从3.0版本开始支持 Bash. 可以很容易地通过源代码来安装. 它还能够很好地与图形调试器前端 ddd 配合使用.

Further sources of information

已安装的shell和系统工具的手册是我们最重要的信息来源.

你可以从 [Tea13] 和 [Ste13] 这两个地方找到其他的风格指引. 有时也会有一些关于shell变成的科技文章发表. 另外市面上也已经有了许多关于shell变成的书籍可以阅读. 如果对系统编程和安全方面有疑问,[GSS03 , Whe03]是很好的入手资料. 网上也有很多关注于平台安全事物和新开发技术的网站.

References

[Bas13] The Bash-Hackers Wiki. http://wiki.bash-hackers.org/scripting/nonportable , 2013 [Bur04] Burtch , Ken O.: Linux Shell Scripting with Bash (Developer's Library). Sams, 2004. -ISBN 0672326426. - As PDF freely available from the publisher. [Coo12] Cooper , Mendel: Advanced Bash-Scripting Guide. http://www.tldp.org/LDP/abs/html/, 2012. Comprehensive tutorial with many examples, available in several formats. Well suited for additional online help and as reference work. [FSF10] FSF : Bash Reference Manual . Free Software Foundation : http://www.gnu.org, 12 2010. - Bash shell, version 4.2. The official manual. [GSS03] Garfinkel , Simson ; Spafford , Gene ; Schwartz , Alan: Practical Unix & Internet Security (3rd Edition) . O'Reilly Media, 2003. - ISBN 0596003234 [Lhu13] Lhunath : BashGuide . http://mywiki.wooledge.org/BashGuide, 2013 [NR05] Newham , Cameron ; Rosenblatt , Bill: Learning the bash Shell (3rd Edition) . O'Reilly Media, 2005. - ISBN 0596009658. - Textbook; covers the features of Bash Version 3.0. [POS13] The Open Group Base Specifications Issue 7. http://pubs.opengroup.org/onlinepubs/9699919799/, 2013 [Ste13] Steven , Heiner: Heiner's SHELLdorado . http://shelldorado.com/goodcoding, 2013 [Tea13] Team , Inquisitor: Coding style guidelines: Shell script. http://www.inquisitor.ru/doc/coding-style-shell.html, 2013 [Whe03] Wheeler , David A.: Secure Programming for Linux and Unix HOWTO . March 2003. - Version v3.010