对于一个好的bash/ksh脚本模板,你有什么建议可以用作所有新创建的脚本的标准?
我通常#!
使用带有文件名,概要,用法,返回值,作者,更改日志的注释掉头开始(在行之后)并且适合80-char行.
所有文档行我都以双哈希符号开头,##
因此我可以轻松地为它们grep,本地var名称前面加上"__".
还有其他最佳做法吗?提示?命名约定?返回代码怎么样?
关于版本控制的评论:我们使用SVN可以,但企业中的另一个部门有一个单独的repo,这是他们的脚本.如果没有@author信息,我如何知道与Q联系的人?使用类似于javadocs的条目即使在shell上下文中也有一些优点,恕我直言,但我可能错了.
我将Norman的答案扩展到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
命令确保状态为非零,表示某种错误.''trap
'为0表示如果shell因任何原因退出,也会执行代码 - 它在标记为"实际工作"的部分中包含粗心.最后的代码然后删除任何幸存的临时文件,然后在退出时解除陷阱,最后以零(成功)状态退出.显然,如果你想以其他状态退出,你可以 - 只需确保在运行rm
和trap
行之前将其设置在变量中,然后使用exit $exitval
.
我通常使用以下命令从脚本中删除路径和后缀,因此我可以$arg0
在报告错误时使用:
arg0=$(basename $0 .sh)
我经常使用shell函数来报告错误:
error() { echo "$arg0: $*" 1>&2 exit 1 }
如果只有一个或两个错误退出,我不打扰该功能; 如果还有,我这样做是因为它简化了编码.我还创建了或多或少精心设计的函数,usage
以便给出如何使用命令的摘要 - 再次,只有在不止一个地方使用它时.
另一个相当标准的片段是一个选项解析循环,使用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
.这有助于我在行为不同的时候 - 我可以选择操作版本 - 或者在开发脚本时(我可以在不修改脚本的情况下为命令添加额外的仅调试选项).(有关相关符号的信息,请参阅Shell参数扩展${VAR:=value}
.)
我使用第一组##行作为使用文档.我现在不记得我第一次看到这个.
#!/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. }
这是我用于脚本shell(bash或ksh)的头文件.它man
看起来很相似,也用于显示用法().
#!/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
任何将在野外发布的代码都应该有以下短标题:
# Script to turn lead into gold # Copyright (C) 2009 Ima Hacker (i.m.hacker@foo.org) # 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的人很好:)
我可以给出的唯一其他建议是在here-docs中观察你的扩展,即
cat << EOF > foo.sh printf "%s was here" "$name" EOF
...当你可能想要保留变量时,将扩展$ name.解决这个问题:
printf "%s was here" "\$name"
这会将$ name作为变量,而不是扩展它.
我还强烈建议学习如何使用陷阱捕获信号..并使用这些处理程序作为样板代码.使用简单的SIGUSR1告诉正在运行的脚本减速非常方便:)
我编写的大多数新程序(面向工具/命令行)最初都是shell脚本,它是UNIX工具原型的好方法.
您可能也喜欢SHC shell脚本编译器,请在此处查看.
启用错误检测可以更容易地及早发现脚本中的问题:
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
我的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==========