对于一个好的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因任何原因退出,代码也会被执行 – 它包含标记为“实际工作”的部分中的粗心大意。 最后的代码会在退出陷阱之前删除任何存活的临时文件,最后以零(成功)状态退出。 显然,如果你想以另一种状态退出,你可以 – 只要确保在运行rm
和trap
行之前将其设置为一个变量,然后使用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脚本? 我得到了威士忌。
建议:
文档应该是数据或代码,而不是评论。 至少有一个usage()
函数。 看看ksh和其他AST工具如何用每个命令上的–man选项来记录自己。 (不能链接,因为网站已关闭。)
用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
你可以做的是做一个脚本,为脚本创建一个标题,并让它在你喜欢的编辑器中自动打开。 我看到有一个人在这个网站做这件事:
#!/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解释符号(或任何解释的符号)在文件名或任何名称的事情。 特别是…“”“$&*#(){} [] – ,我用空格。
请记住,这只是公约。 最好的做法,粗糙,但有时你被迫在线以外。 最重要的是在你的项目中保持一致。