web-dev-qa-db-ja.com

シェルスクリプトテンプレート

新しく作成されたすべてのスクリプトの標準として使用する良いbash/kshスクリプトテンプレートの提案は何ですか?

私は通常、(#!行の後に)ファイル名、概要、使用法、戻り値、作成者、変更ログをコメントアウトしたヘッダーで開始し、80文字の行に収まります。

私がダブルハッシュ記号##で開始するすべてのドキュメント行は、簡単にgrepできるようにして、ローカル変数名の先頭に "__"を付加しています。

他のベストプラクティスはありますか?チップ?命名規則?戻りコードはどうですか?

バージョン管理に関するコメント:私たちはSVNを大丈夫に使用していますが、企業内の別の部署が別のリポジトリを持っています。これが彼らのスクリプトです。 @author情報がない場合、Qに連絡する相手を知るにはどうすればよいですか? javadocsのようなエントリを使用すると、シェルのコンテキストであるIMHOでもいくつかのメリットがありますが、私は間違っている可能性があります。

23
webwesen

ノーマンの答えを6行に拡張しますが、最後の行は空白です。

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

3行目はバージョン管理識別文字列です。これは実際には、SCCSマーカー '@(#)'とのハイブリッドであり、(SCCS)プログラムwhatおよびRCSバージョン文字列によって識別できます。ファイルがRCS(私が私的に使用するデフォルトのVCS)の下に置かれると展開されます。 RCSプログラムidentは、_$Id$_のように見える_$Id: mkscript.sh,v 2.3 2005/05/20 21:06:35 jleffler Exp $_の拡張形式を取得します。 5行目は、スクリプトの上部に目的の説明が必要であることを思い出させます。私はWordをスクリプトの実際の説明に置き換えます(そのため、たとえばその後にコロンがありません)。

その後、基本的にシェルスクリプトの標準はありません。表示される標準フラグメントはありますが、すべてのスクリプトに表示される標準フラグメントはありません。 (私の議論は、スクリプトが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を含むファイル名プレフィックスを作成します。これはセキュリティ対策ではありません。これは単純な同時実行性の測定であり、スクリプトの複数のインスタンスが互いのデータを踏みにじることを防ぎます。 (セキュリティのために、非パブリックディレクトリでは予測できないファイル名を使用してください。)2行目は、シェルがいずれかを受け取った場合に 'rm'および 'exit'コマンドが確実に実行されるようにしますシグナルSIGHUP(1)、SIGINT(2)、SIGQUIT(3)、SIGPIPE(13)またはSIGTERM(15)。 'rm'コマンドは、テンプレートに一致する中間ファイルをすべて削除します。 exitコマンドは、ステータスがゼロ以外であることを確認し、何らかのエラーを示します。 0の「trap」は、シェルが何らかの理由で終了した場合にもコードが実行されることを意味します。これは、「実際の作業」とマークされたセクションで不注意をカバーします。最後のコードは、残っている一時ファイルをすべて削除し、before終了時にトラップを解除し、最終的にゼロ(成功)ステータスで終了します。明らかに、別のステータスで終了したい場合は、rmおよびtrap行を実行する前に変数に設定していることを確認してから、_exit $exitval_を使用してください。

私は通常、スクリプトからパスとサフィックスを削除するために次を使用するため、エラーを報告するときに_$arg0_を使用できます。

_arg0=$(basename $0 .sh)
_

私はよくエラーを報告するためにシェル関数を使用します:

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

エラー出口が1つまたは2つしかない場合は、関数を気にしません。それ以上ある場合は、コーディングを簡略化するために行います。また、コマンドを使用する方法の概要を示すために、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は累積的ですが、ここで使用されている表記は、引数のスペースを追跡できません。その問題を回避する(非標準の)方法もあります。

最初のシフト表記は任意のシェルで機能します(または、 '$(...)'の代わりにバックティックを使用した場合に機能します。2つ目は最新のシェルで機能します。括弧の代わりに角かっこを使用する代替案もあるかもしれませんが、しかし、これはうまくいくので、私はそれが何であるかを理解するのに気にしませんでした。

今のところ最後のトリックは、GNUと非GNUバージョンのプログラムの両方を頻繁に使用しているので、どちらを使用するかを選択できるようにしたいということです。したがって、多くのスクリプト、 、次のような変数を使用します。

_: ${Perl:=Perl}
: ${SED:=sed}
_

そして、Perlまたはsedを呼び出す必要がある場合、スクリプトは_$Perl_または_$SED_を使用します。これは、動作が異なる場合(運用バージョンを選択できる場合)、またはスクリプトの開発中に役立ちます(スクリプトを変更せずに、コマンドにデバッグ専用オプションを追加できます)。 (_${VAR:=value}_および関連する表記法については、 シェルパラメータの展開 を参照してください。)

21

使用方法のドキュメントには、##行の最初のセットを使用しています。これを最初に見た場所を今は思い出せません。

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

これは、スクリプトシェル(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

実際にリリースされるコードには、次の短いヘッダーが必要です。

# Script to turn lead into gold
# Copyright (C) 2009 Ima Hacker ([email protected])
# Permission to copy and modify is granted under the foo license
# Last revised 1/1/2009

変更ログをコードヘッダーに記録することは、バージョン管理システムがひどく不便だったときからの逆戻りです。最終更新日は、スクリプトの古さを示します。

Bashismに依存する場合は、/ bin/shではなく#!/ bin/bashを使用してください。shはシェルのPOSIX呼び出しであるためです。/bin/shがbashを指している場合でも、/ bin/shを介して実行すると、多くの機能が無効になります。ほとんどのLinuxディストリビューションでは、bashismに依存するスクリプトを使用しないため、移植可能にしてください。

私にとって、シェルスクリプトのコメントは、次のようなものを読まない限り、ばかげています。

# I am not crazy, this really is the only way to do this

シェルスクリプティングは非常に単純であるため、(それを行う方法を誰かに教えるためのデモンストレーションを作成している場合を除き)コードはほとんど常にそれ自体を説明します。

一部のシェルは、型指定された「ローカル」変数を供給したくない。今日まで、Busybox(一般的なレスキューシェル)はその1つだと思います。代わりに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を使用するほとんどの無料ソフトウェアパッケージの「configure」スクリプトを表示するだけで、非常に優れたポータブルシェルコードのひどく狂った例を調べることができます。シェルのようなUNIXを持っている人類に知られているすべてのシステムに対応する6300行のコードがあるので、正気とは言えません。あなたはその種の肥大化を望んでいないが、/ bin/shをzshにポイントするかもしれない人たちにナイスであるなど、内部のさまざまな移植性ハックのいくつかを研究することは興味深い。

私が与えることができる他の唯一のアドバイスは、ヒアドキュメントでのあなたの拡張を監視することです。

cat << EOF > foo.sh
   printf "%s was here" "$name"
EOF

...変数をそのままにしたい場合は、$ nameを展開します。これを介してこれを解決します:

  printf "%s was here" "\$name"

$ nameを展開するのではなく、変数として残します。

また、トラップを使用してシグナルをキャッチする方法を学び、定型コードとしてこれらのハンドラーを利用することを強くお勧めします。実行中のスクリプトに単純なSIGUSR1で減速するように指示することは非常に便利です。

私が作成するほとんどの新しいプログラム(ツール/コマンドライン指向)は、シェルスクリプトとして開始されます。これは、UNIXツールのプロトタイプを作成するための優れた方法です。

SHCシェルスクリプトコンパイラ ここで確認する もお勧めです。

7
Tim Post

エラー検出を有効にすると、スクリプトの問題を早期に検出しやすくなります。

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/*グロブが解釈されることを避けてください文字通り

これらすべてを2行で組み合わせることができます。

set -o errexit -o nounset -o noclobber -o pipefail
shopt -s nullglob
6
l0b0

私の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
Hui Zheng

私は提案します

_#!/bin/ksh
_

以上です。シェルスクリプトのヘビーウェイトブロックコメント?ウィリーを手に入れます。

提案:

  1. ドキュメントはコメントではなく、データまたはコードである必要があります。少なくともusage()関数。 kshと他のASTツールがすべてのコマンドで--manオプションを使用して自分自身を文書化する方法を見てください(Webサイトがダウンしているためリンクできません)。

  2. typesetを使用してローカル変数を宣言します。それが目的です。厄介なアンダースコアは必要ありません。

3
Norman Ramsey

一般に、私が書くすべてのスクリプトについて、守りたいいくつかの規則があります。すべてのスクリプトは、他の人が読む可能性があることを前提に記述しています。

すべてのスクリプトをヘッダーで開始し、

#!/bin/bash
# [ID LINE]
##
## FILE: [Filename]
##
## DESCRIPTION: [Description]
##
## AUTHOR: [Author]
##
## DATE: [XX_XX_XXXX.XX_XX_XX]
## 
## VERSION: [Version]
##
## USAGE: [Usage]
##

私はその日付形式を使用して、grep /検索を簡単にしています。 '['括弧を使用して、ユーザーが自分で入力する必要があるテキストを示します。コメントの外にある場合は、 '#['で始めようとします。そうすれば、誰かがそのまま貼り付けても、入力やテストコマンドと間違えられることはありません。例としてこのスタイルを確認するには、manページの使用法のセクションを確認してください。

コード行をコメント化する場合は、単一の「#」を使用します。メモとしてコメントをするときは、二重の「##」を使用します。 /etc/nanorcもその規則を使用します。実行しないように選択されたコメントを区別することは役に立ちます。メモとして作成されたコメントの詩。

私のすべてのシェル変数は、CAPSで行うことを好みます。特に必要がない限り、私は4〜8文字を維持するようにしています。名前は、その使用法と可能な限り関連しています。

また、成功した場合は常に0で、エラーの場合は1で終了します。スクリプトにさまざまな種類のエラーがある場合(そして実際に誰かを助けるか、何らかの方法で何らかのコードで使用される可能性がある場合)、1を超える文書化されたシーケンスを選択します。通常、終了コードは*で厳密に強制されていません。 nixの世界。残念ながら、私は良い一般的な数体系を見つけたことがありません。

私は標準的な方法で引数を処理するのが好きです。私は常にgetoptよりもgetoptsを好みます。 「読み取り」コマンドやifステートメントを使ってハッキングを行うことはありません。ネストされたifsを回避するために、caseステートメントも使用します。私は長いオプションに翻訳スクリプトを使用しているため、-helpはgetoptsに-hを意味します。私はすべてのスクリプトをbash(受け入れ可能な場合)または一般的なshのいずれかで記述します。

私はファイル名にbashで解釈された記号(または解釈された記号)を使用しないでください。特に... "'` $&*#(){} []-、スペースには_を使用します。

これらは慣例にすぎないことを覚えておいてください。ベストプラクティス、粗雑ですが、場合によっては、線の外に追いやられます。最も重要なことは、プロジェクト全体およびプロジェクト内で一貫性を保つことです。

2
J. M. Becker

あなたができることは、スクリプトのヘッダーを作成するスクリプトを作成し、それをお気に入りのエディターで自動的に開くことです。私は男がこのサイトでそれをするのを見ました:

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