27

对于将一个好的 bash/ksh 脚本模板用作所有新创建的脚本的标准,您有什么建议?

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

所有文档行我都以双哈希符号开头,##因此我可以轻松地为它们进行 grep,并且本地 var 名称以“__”开头。

还有其他最佳实践吗?尖端?命名约定?返回码呢?

关于版本控制的评论:我们使用 SVN 可以,但是企业的另一个部门有一个单独的 repo,这是他们的脚本。如果没有@author 信息,我如何知道与 Q 联系的人?即使在 shell 上下文中,使用类似于 javadocs 的条目也有一些优点,恕我直言,但我可能错了。

4

9 回答 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 表示法编写的。关于为什么有人在#!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) rmexitSIGINT (2)、 SIGQUIT (3)、SIGPIPE (13) 或 SIGTERM (15)。' rm' 命令删除任何与模板匹配的中间文件;该exit命令确保状态不为零,表示某种错误。这 'trap' of 0 表示如果 shell 出于任何原因退出,代码也会被执行——它涵盖了标记为“实际工作”部分中的粗心。最后的代码然后删除所有幸存的临时文件,然后在退出时解除陷阱,最后以零(成功)状态退出。显然,如果您想以其他状态退出,您可以 - 只需确保在运行rmandtrap行之前将其设置在变量中,然后使用exit $exitval.

我一般用下面的方法把脚本中的路径和后缀去掉,这样$arg0报错的时候就可以用了:

arg0=$(basename $0 .sh)

我经常使用一个shell函数来报错:

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

如果只有一个或两个错误退出,我不会打扰该功能;如果还有更多,我会这样做,因为它简化了编码。我还创建了或多或少复杂的函数usage,以提供如何使用命令的摘要 - 同样,只有在不止一个地方可以使用它的情况下。

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

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 orsed时,脚本使用$PERLor $SED。当某些行为不同时,这对我很有帮助——我可以选择操作版本——或者在开发脚本时(我可以在不修改脚本的情况下向命令添加额外的仅调试选项)。(有关和相关符号的信息,请参阅Shell 参数扩展。)${VAR:=value}

于 2009-01-10T06:43:25.983 回答
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.
}
于 2011-10-13T14:19:01.927 回答
10

任何将在野外发布的代码都应具有以下短标题:

# 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 的脚本,而是尽量做到可移植。

在继承其他人的脚本时,我发现人们倾向于在不需要的地方(例如# loop over $var)和非常零星地在需要注释地方(例如超长的 Perl 单行代码或带有数十个参数的 JVM 执行)进行大量注释)。这根本不是 shell 脚本所独有的,在许多已建立的代码库中都是一个问题,但在脚本中尤其令人沮丧。我不知道/bin/foo -- {mile long list of arguments}通过查看它会做什么,但我确实知道编写脚本的结构。评论也非常感谢您所做的事情在表面上看起来有点疯狂。

有些 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 是如果有一个 else-if 可以混合使用。

只需查看大多数使用 autoconf 的免费软件包中的“配置”脚本,就可以研究非常好的可移植 shell 代码的一个可怕的疯狂示例。我说疯狂是因为它的 6300 行代码迎合了人类已知的每一个拥有类似 UNIX 外壳的系统。你不想要那种膨胀,但是研究其中的一些各种可移植性黑客是很有趣的..比如对那些可能将 /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 脚本编译器,请在此处查看

于 2009-01-10T07:39:38.693 回答
9

这是我用于脚本外壳(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

于 2015-04-11T14:30:44.773 回答
7

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

set -o errexit

在第一个错误时退出脚本。这样你就可以避免继续做一些依赖于脚本前面的东西的事情,也许最终会出现一些奇怪的系统状态。

set -o nounset

将对未设置变量的引用视为错误。避免运行诸如rm -you_know_what "$var/"unset 之类的东西非常重要$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
于 2011-11-01T14:32:06.123 回答
5

我的 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==========
于 2012-04-18T11:50:25.387 回答
3

我会建议

#!/bin/ksh

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

建议:

  1. 文档应该是数据或代码,而不是注释。至少一个usage()功能。看看 ksh 和其他 AST 工具如何在每个命令上使用 --man 选项记录自己。(无法链接,因为网站已关闭。)

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

于 2009-01-10T05:16:53.567 回答
3

一般来说,对于我编写的每个脚本,我都有一些我喜欢遵守的约定。我编写所有脚本时都假设其他人可能会阅读它们。

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

#!/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 语句来避免嵌套 if。我对长选项使用翻译脚本,所以 --help 意味着 -h 到 getopts。我用 bash(如果可以的话)或通用 sh 编写所有脚本。

我从不在文件名或任何名称中使用 bash 解释符号(或任何解释符号)。具体来说..." ' ` $ & * # () {} [] -,我使用 _ 表示空格。

请记住,这些只是约定。最佳实践,粗略,但有时你被迫在界外。最重要的是在您的项目中和项目中保持一致。

于 2011-10-11T16:36:39.230 回答
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
#===============================================================================
于 2011-10-28T02:54:17.393 回答