2009-01-09 120 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