shell脚本模板

对于一个好的bash / ksh脚本模板,你会有什么build议可以用作所有新创build脚本的标准?

我通常开始(在#!行之后)带有带有文件名,摘要,用法,返回值,作者,更新日志的注释掉的标题,并且适合80个字符的行。

所有的文档行我用双重哈希符号开头:“##”,所以我可以很容易地grep他们和本地var名称前缀“__”。

任何其他最佳实践? 提示? 命名约定? 那么返回码呢?

谢谢

编辑:版本控制的评论:我们得到了svn所有权利,但企业中的另一个部门有一个单独的回购,这是他们的脚本 – 如果没有@author信息,我怎么知道谁联系Q? javadocs类似的条目甚至在shell上下文中也有一些优点,恕我直言,但我可能是错的。 感谢您的回应

我将诺曼的答案扩展到6行,最后一行是空白的:

#!/bin/ksh # # @(#)$Id$ # # Purpose 

第三行是一个版本控制标识字符串 – 它实际上是一个带有SCCS标记“ @(#) ”的混合体,它可以由(SCCS)程序标识出what和一个RCS版本字符串,当文件放在RCS,我用于私人使用的默认VCS。 RCS程序ident拿起$Id$的扩展形式,可能看起来像$Id: mkscript.sh,v 2.3 2005/05/20 21:06:35 jleffler Exp $ 。 第五行提醒我,脚本应该在顶部描述它的目的; 我用脚本的实际描述替换这个单词(例如,这就是为什么后面没有冒号)。

之后,shell脚本基本上没有什么标准。 有标准片段出现,但没有出现在每个脚本中的标准片段。 (我的讨论假设脚本是用Bourne,Korn或者POSIX(Bash)shell符号来写的,关于为什么在#! sigil后面加上C Shell衍生物的人是为什么生活在罪中呢?

例如,只要脚本创建中间(临时)文件,此代码就会以某种形式出现:

 tmp=${TMPDIR:-/tmp}/prog.$$ trap "rm -f $tmp.?; exit 1" 0 1 2 3 13 15 ...real work that creates temp files $tmp.1, $tmp.2, ... rm -f $tmp.? trap 0 exit 0 

第一行选择一个临时目录,如果用户没有指定一个备用目录,则默认为/ tmp($ TMPDIR被广泛认可并由POSIX标准化)。 然后它创建一个包含进程ID的文件名前缀。 这不是安全措施; 这是一个简单的并发措施,防止脚本的多个实例践踏彼此的数据。 (为了安全起见,在非公共目录中使用不可预知的文件名)。第二行确保在shell接收到任何信号SIGHUP(1),SIGINT(2)时执行' rm '和' exit ' ),SIGQUIT(3),SIGPIPE(13)或SIGTERM(15)。 ' rm '命令删除所有匹配模板的中间文件。 exit命令确保状态为非零,表示某种错误。 0的“ trap ”意味着如果shell因任何原因退出,代码也会被执行 – 它包含标记为“实际工作”的部分中的粗心大意。 最后的代码会在退出陷阱之前删除任何存活的临时文件,最后以零(成功)状态退出。 显然,如果你想以另一种状态退出,你可以 – 只要确保在运行rmtrap行之前将其设置为一个变量,然后使用exit $exitval

我通常使用以下命令从脚本中删除路径和后缀,因此在报告错误时可以使用$arg0

 arg0=$(basename $0 .sh) 

我经常使用shell函数来报告错误:

 error() { echo "$arg0: $*" 1>&2 exit 1 } 

如果只有一个或两个错误退出,我不打扰的功能; 如果还有更多,我会这样做,因为它简化了编码。 我也创建了一些或多或少精巧的函数,用来给出如何使用这个命令的总结 – 只有在不止一个地方使用它时。

另一个相当标准的片段是一个选项解析循环,使用内置的getopts shell:

 vflag=0 out= file= Dflag= while getopts hvVf:o:D: flag do case "$flag" in (h) help; exit 0;; (V) echo "$arg0: version $Revision$ ($Date$)"; exit 0;; (v) vflag=1;; (f) file="$OPTARG";; (o) out="$OPTARG";; (D) Dflag="$Dflag $OPTARG";; (*) usage;; esac done shift $(expr $OPTIND - 1) 

要么:

 shift $(($OPTIND - 1)) 

“$ OPTARG”周围的引号处理参数中的空格。 Dflag是累积的,但是这里使用的表示法在参数中失去了对空间的追踪。 有(非标准的)方法来解决这个问题。

第一个移位符号适用于任何shell(或者如果我使用back-ticks而不是' $(...) ',第二个移植符号可以在现代shell中使用;甚至可以使用方括号而不是括号括起来。这个工程,所以我没有打扰到这是什么。

现在最后一招是我经常同时拥有GNU和非GNU版本的程序,我希望能够选择我使用的程序。 因此,我的许多脚本使用的变量如下:

 : ${PERL:=perl} : ${SED:=sed} 

然后,当我需要调用Perl或sed ,脚本使用$PERL$SED 。 这有助于我在某些行为有所不同的情况下 – 我可以选择操作版本 – 或者在开发脚本时(我可以在不修改脚本的情况下为命令添加额外的仅调试选项)。

我使用第一组##行作为使用文档。 现在我不记得我第一次看到这个。

 #!/bin/sh ## Usage: myscript [options] ARG1 ## ## Options: ## -h, --help Display this message. ## -n Dry-run; only show what would be done. ## usage() { [ "$*" ] && echo "$0: $*" sed -n '/^##/,/^$/s/^## \{0,1\}//p' "$0" exit 2 } 2>/dev/null main() { while [ $# -gt 0 ]; do case $1 in (-n) DRY_RUN=1;; (-h|--help) usage 2>&1;; (--) shift; break;; (-*) usage "$1: unknown option";; (*) break;; esac done : do stuff. } 

任何将要在野外发布的代码都应该具有以下简短的头文件:

 # Script to turn lead into gold # Copyright (C) 2009 Joe Q Hacker - All Rights Reserved # Permission to copy and modify is granted under the foo license # Last revised 1/1/2009 

在版本控制系统非常不方便的情况下,保持更改日志进入代码头是一个倒退。 最后一次修改日期显示某人有多大的脚本。

如果你要依靠bashisms,使用#!/ bin / bash而不是/ bin / sh,因为sh是任何shell的POSIX调用。 即使/ bin / sh指向bash,如果通过/ bin / sh运行,许多功能也将被关闭。 大多数Linux发行版都不会采用依赖bashisms的脚本,试图使其具有可移植性。

对我来说,shell脚本中的注释是有点愚蠢的,除非他们看到像这样的东西:

 # I am not crazy, this really is the only way to do this 

Shell脚本非常简单,除非你写了一个演示来教别人怎么做,否则代码几乎总是会自行消失的。

一些shell不喜欢被输入的“本地”变量。 我相信到今天,Busybox(一个常见的救援外壳)就是其中之一。 改为使用GLOBALS_OBVIOUS,它更容易阅读,尤其是在通过/ bin / sh -x ./script.sh进行调试时。

我个人的偏好是让逻辑自己说话,并尽量减少解析器的工作。 例如,很多人可能会写:

 if [ $i = 1 ]; then ... some code fi 

我只是在哪里:

 [ $i = 1 ] && { ... some code } 

同样,有人可能会写:

 if [ $i -ne 1 ]; then ... some code fi 

…我会在哪里:

 [ $i = 1 ] || { ... some code } 

我唯一使用传统的if / then / else的方法是,如果还有其他的东西 – 如果投入混合。

可以通过在大多数使用autoconf的免费软件包中查看“configure”脚本来研究非常好的可移植shell代码的一个令人毛骨悚然的例子。 我说疯了,因为它的6300行代码迎合了每个人都知道的UNIX系统,就像shell一样。 你不想要那种臃肿,但研究一些内部的各种可移植性攻击是很有趣的,比如对那些可能指向/ bin / sh的用户来说zsh 🙂

我可以给的唯一的其他建议是在这里观看你的扩展文档,即

 cat << EOF > foo.sh printf "%s was here" "$name" EOF 

…将要扩展$ name,当你可能想离开变量。 解决这个通过:

  printf "%s was here" "\$name" 

这将把$ name作为一个变量,而不是扩展它。

我也强烈建议学习如何使用陷阱捕捉信号,并利用这些处理程序作为样板代码。 通过一个简单的SIGUSR1来告诉一个正在运行的脚本是非常方便的:)

我编写的大多数新程序(面向工具/命令行)都是以shell脚本的形式出现的,这是一个建立UNIX工具原型的好方法。

你也可能喜欢SHC shell脚本编译器, 在这里检查一下 。

这是我用于我的脚本shell(bash或ksh)的头文件。 它看起来很像,用来显示用法()。

 #!/bin/ksh #================================================================ # HEADER #================================================================ #% SYNOPSIS #+ ${SCRIPT_NAME} [-hv] [-o[file]] args ... #% #% DESCRIPTION #% This is a script template #% to start any good shell script. #% #% OPTIONS #% -o [file], --output=[file] Set log file (default=/dev/null) #% use DEFAULT keyword to autoname file #% The default value is /dev/null. #% -t, --timelog Add timestamp to log ("+%y/%m/%d@%H:%M:%S") #% -x, --ignorelock Ignore if lock file exists #% -h, --help Print this help #% -v, --version Print script information #% #% EXAMPLES #% ${SCRIPT_NAME} -o DEFAULT arg1 arg2 #% #================================================================ #- IMPLEMENTATION #- version ${SCRIPT_NAME} (www.uxora.com) 0.0.4 #- author Michel VONGVILAY #- copyright Copyright (c) http://www.uxora.com #- license GNU General Public License #- script_id 12345 #- #================================================================ # HISTORY # 2015/03/01 : mvongvilay : Script creation # 2015/04/01 : mvongvilay : Add long options and improvements # #================================================================ # DEBUG OPTION # set -n # Uncomment to check your syntax, without execution. # set -x # Uncomment to debug this shell script # #================================================================ # END_OF_HEADER #================================================================ 

以下是使用功能:

  #== needed variables ==# SCRIPT_HEADSIZE=$(head -200 ${0} |grep -n "^# END_OF_HEADER" | cut -f1 -d:) SCRIPT_NAME="$(basename ${0})" #== usage functions ==# usage() { printf "Usage: "; head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#+" | sed -e "s/^#+[ ]*//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g" ; } usagefull() { head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#[%+-]" | sed -e "s/^#[%+-]//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g" ; } scriptinfo() { head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#-" | sed -e "s/^#-//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g"; } 

这是你应该得到的:

 # Display help $ ./template.sh --help SYNOPSIS template.sh [-hv] [-o[file]] args ... DESCRIPTION This is a script template to start any good shell script. OPTIONS -o [file], --output=[file] Set log file (default=/dev/null) use DEFAULT keyword to autoname file The default value is /dev/null. -t, --timelog Add timestamp to log ("+%y/%m/%d@%H:%M:%S") -x, --ignorelock Ignore if lock file exists -h, --help Print this help -v, --version Print script information EXAMPLES template.sh -o DEFAULT arg1 arg2 IMPLEMENTATION version template.sh (www.uxora.com) 0.0.4 author Michel VONGVILAY copyright Copyright (c) http://www.uxora.com license GNU General Public License script_id 12345 # Display version info $ ./template.sh -v IMPLEMENTATION version template.sh (www.uxora.com) 0.0.4 author Michel VONGVILAY copyright Copyright (c) http://www.uxora.com license GNU General Public License script_id 12345 

您可以在这里获取完整的脚本模板: http : //www.uxora.com/unix/shell-script/18-shell-script-template

我的bash模板如下(在我的vim配置中设置):

 #!/bin/bash ## DESCRIPTION: ## AUTHOR: $USER_FULLNAME declare -r SCRIPT_NAME=$(basename "$BASH_SOURCE" .sh) ## exit the shell(default status code: 1) after printing the message to stderr bail() { echo -ne "$1" >&2 exit ${2-1} } ## help message declare -r HELP_MSG="Usage: $SCRIPT_NAME [OPTION]... [ARG]... -h display this help and exit " ## print the usage and exit the shell(default status code: 2) usage() { declare status=2 if [[ "$1" =~ ^[0-9]+$ ]]; then status=$1 shift fi bail "${1}$HELP_MSG" $status } while getopts ":h" opt; do case $opt in h) usage 0 ;; \?) usage "Invalid option: -$OPTARG \n" ;; esac done shift $(($OPTIND - 1)) [[ "$#" -lt 1 ]] && usage "Too few arguments\n" #==========MAIN CODE BELOW========== 

我会建议

 #!/bin/ksh 

就是这样。 重量级块注释的shell脚本? 我得到了威士忌。

建议:

  1. 文档应该是数据或代码,而不是评论。 至少有一个usage()函数。 看看ksh和其他AST工具如何用每个命令上的–man选项来记录自己。 (不能链接,因为网站已关闭。)

  2. typeset声明局部变量。 这就是它的目的。 不需要讨厌的下划线。

启用错误检测功能可以更早地检测脚本中的问题:

 set -o errexit 

首次出错时退出脚本。 这样你就可以避免继续做一些依赖于脚本早期的东西,也许最终会出现一些奇怪的系统状态。

 set -o nounset 

将对未设置变量的引用视为错误。 非常重要的是避免运行诸如rm -you_know_what "$var/"设置$var 。 如果知道该变量可以是未设置的,并且这是一个安全的情况,那么可以使用${var-value}在未设置时使用不同的值,如果未设置则使用${var:-value}使用不同的值空的。

 set -o noclobber 

插入一个>你想插入的地方很容易,并覆盖你想读的文件。 如果您需要在脚本中打开某个文件,则可以在相关行之前禁用该文件,然后再次启用该文件。

 set -o pipefail 

使用一组管道命令的第一个非零退出代码(如果有的话)作为全套命令的退出代码。 这使得更容易调试管道命令。

 shopt -s nullglob 

如果没有匹配该表达式的文件,避免你的/foo/* glob被从字面上解释。

你可以把所有这些分为两行:

 set -o errexit -o nounset -o noclobber -o pipefail shopt -s nullglob 

你可以做的是做一个脚本,为脚本创建一个标题,并让它在你喜欢的编辑器中自动打开。 我看到有一个人在这个网站做这件事:

http://code.activestate.com/recipes/577862-bash-script-to-create-a-header-for-bash-scripts/?in=lang-bash

 #!/bin/bash - #title :mkscript.sh #description :This script will make a header for a bash script. #author :your_name_here #date :20110831 #version :0.3 #usage :bash mkscript.sh #notes :Vim and Emacs are needed to use this script. #bash_version :4.1.5(1)-release #=============================================================================== 

一般来说,对于我写的每个脚本,我都会遵守一些惯例。 我写所有的脚本,假设其他人可能会读它们。

我用我的标题开始每个脚本,

 #!/bin/bash # [ID LINE] ## ## FILE: [Filename] ## ## DESCRIPTION: [Description] ## ## AUTHOR: [Author] ## ## DATE: [XX_XX_XXXX.XX_XX_XX] ## ## VERSION: [Version] ## ## USAGE: [Usage] ## 

我使用这种日期格式,以便于grep /搜索。 我用'['括号来表示人们需要进入自己的文本。 如果它们发生在评论之外,我尝试以“#['开头。 这样,如果有人粘贴它们,它不会被误认为是输入或测试命令。 查看手册页上的用法部分,以此作为示例。

当我想注释掉一行代码时,我使用了一个“#”。 当我做注释时,我使用双“##”。 /etc/nanorc使用该约定。 我认为有助于区分被选择不执行的评论; 将一个注释创建为一个注释。

我所有的shell变量,我都喜欢在CAPS中做。 除非有其他必要,我会尽量保留4-8个字符。 这些名字尽可能与其使用有关。

如果成功,我也总是以0退出,或者1为错误。 如果脚本有许多不同类型的错误(并且实际上可以帮助某人,或者可能以某种方式在某些代码中使用),那么我会选择一个超过1的记录顺序。一般来说,退出代码不是严格执行*尼克斯世界。 不幸的是,我从来没有找到一个好的通用号码方案

我喜欢以标准的方式处理争论。 我总是喜欢getopts,getopt。 我从来不会用“读取”命令和if语句做一些破解。 我也喜欢使用case语句,以避免嵌套的ifs。 我使用翻译脚本的长选项,所以 – 帮助意味着-h getopts。 我写了所有的脚本在bash(如果可以的话)或通用sh。

我永远不会使用bash解释符号(或任何解释的符号)在文件名或任何名称的事情。 特别是…“”“$&*#(){} [] – ,我用空格。

请记住,这只是公约。 最好的做法,粗糙,但有时你被迫在线以外。 最重要的是在你的项目中保持一致。