Shell脚本模板

28
您有没有适用于所有新创建的脚本的好的bash/ksh脚本模板建议?
我通常会在#!行之后开始一个注释的头部,包括文件名、概要、用法、返回值、作者、更改日志,并且每行都限制为80个字符。
我使用双井号符号##开头来标识所有文档行,以便可以轻松地使用grep进行查找,并且本地变量名以“__”为前缀。
还有其他最佳实践吗?提示?命名约定?返回代码呢?
关于版本控制的评论:我们使用SVN,但企业中的另一个部门有单独的存储库,并且这是他们的脚本。如果没有作者信息,我如何知道联系哪些人询问问题?在shell环境中,使用类似javadocs的条目甚至也具有一些优点,但我的想法可能是错误的。

4
采用版本控制系统!作者(们)?变更日志? - derobert
我并不认为在本地变量名称前使用__前缀是有益的。 - Jonathan Leffler
9个回答

24

我会将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符号编写的。关于为什么在#!符号后放置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)、SIGQUIT(3)、SIGPIPE(13)或 SIGTERM(15)时执行“rm”和“exit”命令。rm 命令删除与模板匹配的所有中间文件;exit 命令确保状态为非零,表示某种错误。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(如果我使用反引号而不是“$(...)”则会这样做)。第二个适用于现代shell;甚至可能有一种使用方括号而不是圆括号的替代方法,但这个方法有效,所以我没有费心去找出那是什么。

现在最后一个技巧是,我经常同时拥有GNU版本和非GNU版本的程序,并且我想能够选择使用哪个。因此,我的许多脚本使用变量,例如:

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

当我需要调用Perl或sed时,脚本使用$PERL$SED。这有助于我在某些情况下选择操作版本,或者在开发脚本时添加额外的仅限于调试的选项而无需修改脚本。(有关${VAR:=value}和相关符号的信息,请参见Shell参数扩展。)


嗨@Jonathan,符号“:$ {VAR:= file}”是什么意思?提前致谢。 - tmow
2
@tmow:符号${VAR:=file}的意思是,如果$VAR被设置为非空值,则使用该值,但如果$VAR未设置或设置为空字符串,则使用值file并将$VAR设置为该值。因此,它有点像(但比):[ -z "$VAR" ] && VAR=file; echo $VAR更短。 - Jonathan Leffler

17

我使用这一组##行作为用法文档。我现在想不起来我最初在哪里看到它了。

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

3
еҰӮжһңи„ҡжң¬жү§иЎҢдәҶд»»дҪ•cdе‘Ҫд»Өдё”$0дёҚжҳҜз»қеҜ№ж–Ү件еҗҚпјҢдҪҝз”ЁgrepеҜ»жүҫи„ҡжң¬зҡ„usage()еҮҪж•°зңӢиө·жқҘеҫҲй…·пјҢдҪҶдјҡеӨұиҙҘгҖӮжҲ‘е»әи®®е°Ҷusage()еҮҪж•°е®һйҷ…дёҠиҫ“еҮә/жү“еҚ°/жҳҫзӨәдҪҝз”Ёж¶ҲжҒҜгҖӮ - Jens
1
你可以确定脚本的绝对路径并将其存储在变量中。在注释中将使用信息放在顶部可能很好。如果这样做,那么从注释中打印出信息来搜索脚本会使代码更加DRY。 - toxalot
@Jens,在调用usage/help之前几乎没有必要调用cd,而且你提出的替代方案很糟糕。我同意toxalot的观点,只需cat程序文本,就可以在纯文本中看到它所做的概要。 - André Werlang

11

任何准备发布到公开环境的代码,都应该包含以下短头部:

# 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

在代码头部保持变更日志是版本控制系统非常不方便的时代遗留下来的一种做法。最后修改日期显示了脚本的年龄。

如果您要依赖于bashism,请使用#!/bin/bash而不是/bin/sh,因为sh是任何shell的POSIX调用方式。即使/bin/sh指向bash,如果通过/bin/sh运行它,许多功能也将被关闭。大多数Linux发行版都不会接受依赖于bashism的脚本,请尽量做到可移植性。

当涉及到继承他人的脚本时,我发现人们往往在不需要注释的地方进行大量注释(例如#循环$var)并且在需要注释的地方(例如超长Perl one-liner或具有几十个参数的JVM执行)却很零散。这在很多已经建立的代码库中都是一个问题,但在脚本中尤其令人沮丧。我不知道/bin/foo -- {mile long list of arguments}通过查看它可以做什么,但我确实知道如何编写脚本构造。在你正在做一些表面上看起来有点疯狂的事情时,评论也会非常受欢迎。

有些Shell不喜欢输入类型为“local”的变量。我认为直到今天,Busybox(常用的救援Shell之一)仍然是这种情况。因此,使用明显的全局变量名称更容易阅读,特别是在通过/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 
}

只有当存在 else-if 的情况下,我才会使用传统的 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作为变量而不是展开它,是一个很好的习惯。

我也强烈建议学习如何使用trap捕获信号,并将这些处理程序用作样板代码。使用简单的SIGUSR1告诉运行中的脚本减速是非常方便的 :)

我写的大多数新程序(面向工具/命令行)都以shell脚本开始,这是原型制作UNIX工具的好方法。

您可能还喜欢SHC shell脚本编译器,请在此处查看


5
如果您不想扩展 here docs,请使用 << 'EOF' 来抑制扩展。仅在您希望有些内容被展开,有些内容不被展开时使用反斜杠。 - Jonathan Leffler
1
有无数个原因让人们想要在shell脚本中添加注释。说注释是愚蠢的建议是可怕的。 - Dan Grahn
@DanGrahn 哇,2009年的我并不总是能够清楚地表达我的意思 :) 我并不想告诉大家所有的评论都是愚蠢的,而是在那些很难通过阅读代码来理解的事情上(例如,“在这里循环”),缺少了评论。编辑过了,有改善吗? - Tim Post
@TimPost好多了!抱歉这个帖子已经很老了,我没有意识到。说实话,我可以用一只手数出我看过代码并说“太多注释”的次数。 - Dan Grahn
1
@DanGrahn我认为“糟糕”的代码注释并不存在(除非它具有误导性),但是在显而易见的部分周围有过多的注释,而在不太明显的部分则缺乏注释,这是DevOps基本上正在形成时的一个令人发狂的主题。那就是我写这篇评论时我的头脑所在之处。我喜欢像这个回答一样让我回到旧时代的机会:) - Tim Post

10

这是我在bash或ksh脚本中使用的头部。 它类似于man,用于显示usage()。

#!/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

5
你的帖子没有包含完整的模板,而你的网站将代码隐藏在“社交点赞墙”之后,这应该被视为不良行为。 - Hultner
@Hultner 这里有一个备份。我在脚本的最后添加了归属信息。 - Mr. Polywhirl
@Mr.Polywhirl,你的链接也无法使用。 “错误,这是私人粘贴或正在等待审核。如果此粘贴属于您,请登录到Pastebin上查看。” 如果您直接在这里发布答案,那会更好。我会给你点赞的。 - Hultner
@Hultner 对不起,我已经修复了。 - Mr. Polywhirl

8

启用错误检测可以更轻松地及早检测脚本中的问题:

set -o errexit

在第一个错误发生时退出脚本。这样可以避免继续执行依赖脚本中早期某些操作的后续操作,可能会导致一些奇怪的系统状态。

set -o nounset

将对未设置变量的引用视为错误。非常重要,以避免在未设置$var的情况下运行rm -you_know_what "$var/"等命令。如果您知道该变量可能未被设置,并且这是一个安全的情况,您可以使用${var-value}来在其未设置时使用不同的值,或者使用${var:-value}在其未设置为空时使用不同的值。

set -o noclobber

在插入 < 的地方意外插入了 >,覆盖掉了你本想读取的文件是很容易犯的错误。如果您需要在脚本中强制覆盖一个文件,可以在相关行之前禁用此功能,然后再重新启用。

set -o pipefail

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

shopt -s nullglob

避免如果没有文件匹配表达式/foo/*,则该表达式被字面理解。
您可以用两行将它们组合起来:
set -o errexit -o nounset -o noclobber -o pipefail
shopt -s nullglob

5
我的bash模板如下(设置在我的vim配置中):vim configuration
#!/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==========

3
通常,我在写任何脚本时都会遵循一些惯例。 我会以其他人也可能阅读的前提来编写所有脚本。
我会在每个脚本开头加上我的标头。
#!/bin/bash
# [ID LINE]
##
## FILE: [Filename]
##
## DESCRIPTION: [Description]
##
## AUTHOR: [Author]
##
## DATE: [XX_XX_XXXX.XX_XX_XX]
## 
## VERSION: [Version]
##
## USAGE: [Usage]
##

我使用那种日期格式,以便更容易地进行grep/search操作。 我使用'['括号来指示人们需要输入的文本。 如果它们出现在注释之外,我会尝试以'#['开头。 这样,如果有人将它们粘贴为原样,就不会被误认为是输入或测试命令。请查看man页上的用法部分,以查看这种风格作为示例。
当我想注释掉一行代码时,我使用单个'#'。当我做一个注释作为注释时,我使用双倍'##'。 /etc/nanorc也使用了这种约定。我发现这很有帮助,以区分选择不执行的注释和创建为注释的注释。
我所有的shell变量都喜欢用大写字母表示。我尽量保持在4-8个字符之间,除非必要。名称尽可能与其用途相关。
如果成功,我也总是以0退出,或者以1表示错误。如果脚本有许多不同类型的错误(并且实际上会帮助某些人,或者可以在某些代码中使用),我会选择文档化的顺序超过1。 总的来说,在*nix世界中,退出代码并没有得到严格执行。不幸的是,我从未找到过一个好的通用数字方案。
我喜欢按照标准方式处理参数。我总是更喜欢getopts而不是getopt。我永远不会用'read'命令和if语句做一些hack操作。我还喜欢使用case语句,以避免嵌套的ifs。我使用一个翻译脚本来处理长选项,所以--help表示-h以获取getopts。我在bash(如果可以接受)或通用sh中编写所有脚本。
我从不在文件名中(或任何名称中)使用bash解释符号(或任何解释符号)。 具体来说... " ' ` $ & * # () {} [] -,我用_代替空格。
记住,这些只是约定。最佳实践,当然,但有时你被迫走出界限。最重要的是在项目内外保持一致性。

3
我建议:
#!/bin/ksh

就是这样了。对于shell脚本来说,重量级的块注释会让我感到恶心。

建议:

  1. 文档应该是数据或代码,而不是注释。至少需要一个usage()函数。可以看一下ksh和其他AST工具如何在每个命令上使用--man选项进行自我文档化。(无法链接,因为网站已经关闭。)

  2. 使用typeset声明局部变量。这就是它的作用。不需要使用令人讨厌的下划线。


3
你可以编写一个脚本来创建一个脚本头,并让它自动在你喜欢的编辑器中打开。我在这个网站上看到有人这样做过:

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
#===============================================================================

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接