2009-01-09 81 views
20

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

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

所有文档行我以双重哈希符号开头:“##”,所以我可以很容易地grep它们,本地var名称前面加上“__”。

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

感谢

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

+4

作者(s)?更新日志? 获取版本控制系统! – derobert 2009-01-10 06:04:18

回答

18

我诺曼的答案扩展到6线,最后那些空白:

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

第三行是一个版本控制标识字符串 - 这其实是一个SCCS混合(SCCS)程序what可识别的标记'@(#)'以及RCS版本字符串,该字符串在文件置于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),则执行'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 
} 

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

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

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。当某些行为有所不同时,这可以帮助我 - 我可以选择操作版本 - 或者在开发脚本时(我可以在不修改脚本的情况下向命令添加额外的仅调试选项)。

+0

嗨@Jonathan什么是符号“:$ {VAR:= file}”?在此先感谢 – tmow 2012-03-30 14:37:43

+1

@tmow:符号'$ {VAR:= file}`意味着如果`$ VAR`设置为非空值,则使用该值,但如果未设置“$ VAR”或者设置为空字符串,然后使用值`file`,并将$ VAR设置为该值。所以,它有点像(但比它短):`[-z“$ VAR”] && VAR = file; echo $ VAR`。 – 2012-03-31 01:59:29

+0

thx很多,它真的很有用! – tmow 2012-04-04 12:17:03

3

我建议

#!/bin/ksh 

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

建议:

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

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

7

任何代码会在野外被释放应具有以下短标题:

# 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/th如果还有其他 - 如果投入混合,en/else。

只需在大多数使用autoconf的免费软件包中查看'configure'脚本,就可以研究非常好的可移植shell代码的一个令人毛骨悚然的例子。我说疯了,因为它的6300行代码满足了人们熟知的每个系统都有类似于UNIX的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脚本编译器check it out here

+3

如果您不想在此处扩展文档,请使用<<'EOF'来抑制扩展。只有当你想扩展一些东西并且有些东西没有扩展时才使用反斜杠。 – 2009-01-10 18:48:44

1

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

我开始每个脚本与我的头,

#!/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。我使用翻译脚本进行长时间选择,因此--help表示-h表示getopts。我将所有脚本写入bash(如果可接受)或通用sh。

我从来没有在文件名或任何名称中使用bash解释符号(或任何解释符号)。 特别是...“'``$ & *#(){} [] - ,我用_作为空格

请记住,这些只是惯例,最好的做法是粗略的,但有时你被迫在行。最重要的是要跨越,你的项目中是一致的。

9

我用第一套##线使用的文档。我现在不记得在那里我第一次看到这一点。

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

,你能做些什么,是使一个脚本&创建一个头一个脚本,并使其自动在你喜欢的编辑器打开。我看到一个人做,在这个网站:

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

启用错误检测使得它更容易早期发现脚本的问题:在第一个错误

set -o errexit 

退出脚本。这样你就可以避免继续在脚本的前面做某些事情,这可能会导致一些奇怪的系统状态。

set -o nounset 

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

set -o noclobber 

这很容易使插入>,你打算插入<的错误,并覆盖一些文件,你的意思是阅读。如果您需要在脚本中打开文件,可以在相关行之前禁用该文件,然后再次启用该文件。

set -o pipefail 

使用一组管道命令的第一个非零退出代码(如果有的话)作为完整命令集的退出代码。这使得更容易调试管道命令。您/foo/*水珠被解释字面上如果没有匹配表达式的文件

shopt -s nullglob 

避免。

您可将所有的这些在两行:

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

我的bash的模板是如下(在设置我的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========== 
5

这是我用我的脚本头shell(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/%[email protected]%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/%[email protected]%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