2008-09-17 20 views
141

シェルスクリプト(sh、bashなど)のベストプラクティスまたはデザインパターンについて話している人はいますか?シェルスクリプトのデザインパターンまたはベストプラクティス

+0

私はちょうど昨晩[BASHでのテンプレートパターン](http://quickshiftin.com/blog/2014/01/template-method-pattern-bash/)にはほとんどの記事を書きました。あなたの考えを見てください。 – quickshiftin

答えて

190

私は非常に複雑なシェルスクリプトを書いていました。私の最初の提案は「しない」です。その理由は、あなたのスクリプトを妨げる小さなミスをするのはかなり簡単ですし、危険なものにすることさえあります。

私はあなたが、私の個人的な経験を渡すために、他のリソースを持っていない、と述べました。 これは私が普通にやっていることですが、過剰ですが、固体である傾向がありますが、非常に詳細です。

呼び出し

スクリプトがロングとショートのオプションを受け入れます。オプション、getoptとgetoptsを解析する2つのコマンドがあるので注意してください。問題が少ないほどgetoptを使用してください。

もう1つの重要な点は、プログラムが正常に完了するとゼロを返し、何か問題が生じた場合はゼロでないことです。

機能あなただけの呼び出しの前にそれらを定義するために覚えて、bashで関数を呼び出すことができます

を呼び出します。関数はスクリプトのようなもので、数値のみを返すことができます。つまり、文字列値を返すためには別の戦略を策定する必要があります。私の戦略は、結果を格納するためにRESULTという変数を使用し、関数が正常に完了した場合は0を戻すことです。 また、0以外の値を返す場合は例外を発生させ、2つの "例外変数"(mine:EXCEPTIONおよびEXCEPTION_MSG)を設定することができます。最初は例外タイプ、2番目は人間が判読可能なメッセージです。

関数を呼び出すと、関数のパラメータが特殊な変数$ 0、$ 1などに割り当てられます。それらをより意味のある名前にすることをお勧めします。あなたはそれ以外の場合は、未設定の変数が空の文字列として使用されて宣言しない限り、bashで

function foo { 
    local bar="$0" 
} 

エラーを起こしやすい状況:ローカルとして関数内の変数を宣言します。誤って入力された変数は報告されず、空であると評価されるので、これはタイプミスの場合には非常に危険です。これが発生しないように、

set -o nounset 

を使用してください。ただし、これを行うと、未定義の変数を評価するたびにプログラムが中止されるため、注意してください。このため、変数が定義されていないかどうかを確認するための唯一の方法は以下の通りです:

if test "x${foo:-notset}" == "xnotset" 
then 
    echo "foo not set" 
fi 

あなたは読み取り専用として変数を宣言することができます。

readonly readonly_var="foo" 

モジュール化

あなたが達成することができます次のコードを使用した場合にモジュール化「のようなパイソン」:

set -o nounset 
function getScriptAbsoluteDir { 
    # @description used to get the script path 
    # @param $1 the script $0 parameter 
    local script_invoke_path="$1" 
    local cwd=`pwd` 

    # absolute path ? if so, the first character is a/
    if test "x${script_invoke_path:0:1}" = 'x/' 
    then 
     RESULT=`dirname "$script_invoke_path"` 
    else 
     RESULT=`dirname "$cwd/$script_invoke_path"` 
    fi 
} 

script_invoke_path="$0" 
script_name=`basename "$0"` 
getScriptAbsoluteDir "$script_invoke_path" 
script_absolute_dir=$RESULT 

function import() { 
    # @description importer routine to get external functionality. 
    # @description the first location searched is the script directory. 
    # @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable 
    # @param $1 the .shinc file to import, without .shinc extension 
    module=$1 

    if test "x$module" == "x" 
    then 
     echo "$script_name : Unable to import unspecified module. Dying." 
     exit 1 
    fi 

    if test "x${script_absolute_dir:-notset}" == "xnotset" 
    then 
     echo "$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying." 
     exit 1 
    fi 

    if test "x$script_absolute_dir" == "x" 
    then 
     echo "$script_name : empty script path. Dying." 
     exit 1 
    fi 

    if test -e "$script_absolute_dir/$module.shinc" 
    then 
     # import from script directory 
     . "$script_absolute_dir/$module.shinc" 
    elif test "x${SHELL_LIBRARY_PATH:-notset}" != "xnotset" 
    then 
     # import from the shell script library path 
     # save the separator and use the ':' instead 
     local saved_IFS="$IFS" 
     IFS=':' 
     for path in $SHELL_LIBRARY_PATH 
     do 
      if test -e "$path/$module.shinc" 
      then 
       . "$path/$module.shinc" 
       return 
      fi 
     done 
     # restore the standard separator 
     IFS="$saved_IFS" 
    fi 
    echo "$script_name : Unable to find module $module." 
    exit 1 
} 

あなたは、次の構文

輸入「AModule/ModuleFile」SHELL_LIBRARY_PATHで検索されます

と.shinc拡張子を持つファイルをインポートすることができます。グローバル名前空間で常にインポートするので、すべての関数と変数に適切な接頭辞を付けることを忘れないでください。私は二重のアンダースコアをpythonの点として使用します。また

は、お使いのモジュールで最初のものとしてこれを置く

# avoid double inclusion 
if test "${BashInclude__imported+defined}" == "defined" 
then 
    return 0 
fi 
BashInclude__imported=1 

オブジェクト指向あなたがの割り当ての非常に複雑なシステムを構築しない限り、 bashで

が、あなたは、オブジェクト指向プログラミングを行うことができないプログラミングオブジェクト(私はそれについて考えました、それは実行可能ですが、非常識です)。 しかし、実際には、 "シングルトン指向プログラミング"を行うことができます。つまり、各オブジェクトのインスタンスが1つしかなく、1つだけです。

は私が何をすべきかです:私は(モジュール化の項目を参照してください)モジュールにオブジェクトを定義します。その後、私は、このコード例のように、空の(メンバ変数に類似)init関数(コンストラクタ)VARSとメンバ関数を定義

# avoid double inclusion 
if test "${Table__imported+defined}" == "defined" 
then 
    return 0 
fi 
Table__imported=1 

readonly Table__NoException="" 
readonly Table__ParameterException="Table__ParameterException" 
readonly Table__MySqlException="Table__MySqlException" 
readonly Table__NotInitializedException="Table__NotInitializedException" 
readonly Table__AlreadyInitializedException="Table__AlreadyInitializedException" 

# an example for module enum constants, used in the mysql table, in this case 
readonly Table__GENDER_MALE="GENDER_MALE" 
readonly Table__GENDER_FEMALE="GENDER_FEMALE" 

# private: prefixed with p_ (a bash variable cannot start with _) 
p_Table__mysql_exec="" # will contain the executed mysql command 

p_Table__initialized=0 

function Table__init { 
    # @description init the module with the database parameters 
    # @param $1 the mysql config file 
    # @exception Table__NoException, Table__ParameterException 

    EXCEPTION="" 
    EXCEPTION_MSG="" 
    EXCEPTION_FUNC="" 
    RESULT="" 

    if test $p_Table__initialized -ne 0 
    then 
     EXCEPTION=$Table__AlreadyInitializedException 
     EXCEPTION_MSG="module already initialized" 
     EXCEPTION_FUNC="$FUNCNAME" 
     return 1 
    fi 


    local config_file="$1" 

     # yes, I am aware that I could put default parameters and other niceties, but I am lazy today 
     if test "x$config_file" = "x"; then 
      EXCEPTION=$Table__ParameterException 
      EXCEPTION_MSG="missing parameter config file" 
      EXCEPTION_FUNC="$FUNCNAME" 
      return 1 
     fi 


    p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e " 

    # mark the module as initialized 
    p_Table__initialized=1 

    EXCEPTION=$Table__NoException 
    EXCEPTION_MSG="" 
    EXCEPTION_FUNC="" 
    return 0 

} 

function Table__getName() { 
    # @description gets the name of the person 
    # @param $1 the row identifier 
    # @result the name 

    EXCEPTION="" 
    EXCEPTION_MSG="" 
    EXCEPTION_FUNC="" 
    RESULT="" 

    if test $p_Table__initialized -eq 0 
    then 
     EXCEPTION=$Table__NotInitializedException 
     EXCEPTION_MSG="module not initialized" 
     EXCEPTION_FUNC="$FUNCNAME" 
     return 1 
    fi 

    id=$1 

     if test "x$id" = "x"; then 
      EXCEPTION=$Table__ParameterException 
      EXCEPTION_MSG="missing parameter identifier" 
      EXCEPTION_FUNC="$FUNCNAME" 
      return 1 
     fi 

    local name=`$p_Table__mysql_exec "SELECT name FROM table WHERE id = '$id'"` 
     if test $? != 0 ; then 
     EXCEPTION=$Table__MySqlException 
     EXCEPTION_MSG="unable to perform select" 
     EXCEPTION_FUNC="$FUNCNAME" 
     return 1 
     fi 

    RESULT=$name 
    EXCEPTION=$Table__NoException 
    EXCEPTION_MSG="" 
    EXCEPTION_FUNC="" 
    return 0 
} 

私がキャッチする、これは有用であることが見出さ捕捉し処理する信号

と例外を処理します。

function Main__interruptHandler() { 
    # @description signal handler for SIGINT 
    echo "SIGINT caught" 
    exit 
} 
function Main__terminationHandler() { 
    # @description signal handler for SIGTERM 
    echo "SIGTERM caught" 
    exit 
} 
function Main__exitHandler() { 
    # @description signal handler for end of the program (clean or unclean). 
    # probably redundant call, we already call the cleanup in main. 
    exit 
} 

trap Main__interruptHandler INT 
trap Main__terminationHandler TERM 
trap Main__exitHandler EXIT 

function Main__main() { 
    # body 
} 

# catch signals and exit 
trap exit INT TERM EXIT 

Main__main "[email protected]" 

ヒント

何かが何らかの理由で動作しない場合は、コードの順序を変更してみてください。順序は重要であり、必ずしも直感的ではない。

も、tcshのでの作業は考えていません。それは機能をサポートしていません、そして、それは一般的に恐ろしいです。

は注意してくださいが、それは、お役に立てば幸いです。ここで書いたようなものを使う必要があるのなら、それはあなたの問題がシェルで解決するには複雑すぎることを意味します。別の言語を使用してください。私は人間の要因と遺産のためにそれを使用しなければなりませんでした。

+6

うわー、とにかく、私はbashで過度の攻撃をしていると思っていました...私は、分離された機能を使い、サブシェルを悪用する傾向があります。グローバル変数はこれまでになく、内外でも(健全性を維持するため)。すべてが標準出力またはファイル出力を通じて返されます。 set -u/set -e(あまりにも悪いset -eは、最初のifと同じくらい早く無駄になり、多くの場合、自分のコードのほとんどがそこにあります)。関数の引数は[local something = "$ 1"; shift](リファクタリング時の並べ替えを容易にします)。 1つの3000行のbashスクリプトの後で、私はこの方法でも最小のスクリプトを書く傾向があります... – Eugene

+5

ur狂った科学者 – Prospero

+0

モジュール化のための小さな訂正:の後に戻る必要があります。 "$ script_absolute_dir/$ module.shinc" 警告が表示されないようにしてください。 2 $ SHELL_LIBRARY_PATHのモジュールを見つける前にIFS = "$ saved_IFS"を設定する必要があります – Duff

8

Easy: シェルスクリプトの代わりにpythonを使用します。 スクリプトの一部を関数、オブジェクト、永続オブジェクト(zodb)、分散オブジェクト(pyro)にほとんど展開せずに保存することができるため、必要なものを複雑にすることなく、読み込み可能性が100倍近く向上します任意の追加コード。

+7

あなたは「複雑にしなくても」と言った後、あなたはほとんどのケースで醜いモンスターに虐待さの代わりに、問題と実装を簡素化するために使用されている一方で、付加価値を考える様々な複雑さをリストすることによって、自分自身と矛盾します。 – Evgeny

+3

これは大きな欠点を意味します。あなたのスクリプトは、Pythonが存在しないシステムで移植可能ではありません。 – astropanic

+1

これは'08年に回答されたことを認識しています(今は'12年の2日前です)。しかし、この年を見ている人にとっては、PythonやRubyのような言語を利用している人には、それが利用可能である可能性が高いので注意が必要です。もしそうでなければ、インストールされていないコマンド(またはカップルクリック) 。さらに移植性が必要な場合は、Javaでプログラムを書くことを考えてください.JVMを利用できないマシンを見つけるのは難しいでしょう。 –

-1

歳以上は、ジョアンが言ったことと似引用:

「Perlを使ってあなたがそれを使用するbashのを知っているがないことになるでしょう。」

悲しいことに、誰がそれを忘れたのですか?

最近、私はperlの上にpythonをお勧めします。

8

set -eを使用すると、エラーが発生した後に先に進むことはありません。あなたがLinuxで動作させたい場合は、bashに頼らずにsh互換にしてみてください。

20

Bashだけでなく、シェルスクリプトで多くの知恵を得るためにAdvanced Bash-Scripting Guideを見てください。

他の、間違いなく複雑な言語を見てくれる人の話を聞かないでください。シェルスクリプトがあなたのニーズを満たしている場合は、それを使用してください。あなたは機能性を望んでいます。新しい言語はあなたの履歴書に貴重な新しいスキルを提供しますが、それはあなたが仕事をしなければならず、あなたがすでにシェルを知っているなら助けになりません。

前述のように、シェルスクリプトの「ベストプラクティス」や「デザインパターン」はあまりありません。異なる用途には、他のプログラミング言語と同様に、ガイドラインやバイアスが異なります。偉大なセッションが今年(2008年)はちょうどこのトピックにOSCONであった

+7

少し複雑なスクリプトでも、それはベストプラクティスではないことに注意してください。コーディングは、何かを働かせるだけではありません。それは、迅速かつ簡単に構築でき、信頼性が高く、再利用可能で、(特に他の人にとっては)読みやすく維持管理が簡単です。シェルスクリプトはどのレベルにもうまく適合しません。より堅牢な言語は、あらゆるロジックを持つプロジェクトにとってははるかに簡単です。 – drifter

8

それを使用する際に知っています。すばやく汚れている糊付けコマンドを一緒に使用するには問題ありません。 Python、Perl、のモジュール化をにする必要があります。

シェルの最大の問題は、多くの場合、泥の大きなボール、4000行のbash、および成長しているように見えます...そして、あなたのプロジェクト全体がそれに依存するので、それを取り除くことはできません。もちろん、それは美しいbashの40行で始まった。

6

のLinuxディストリビューションの(例えばDebianは)(通常/etc/init.dので見つかった)自分のinit-スクリプトを書く方法を見て、いくつかの「ベストプラクティス」を見つけるために

それらのほとんどは、「bashの-イズム」なしです構成設定、ライブラリー・ファイルおよびソース・フォーマットを良好に分離することができます。

私の個人的なスタイルは、いくつかのデフォルト変数を定義し、新しい値を含むかもしれない設定ファイルをロード( "ソース")しようとするマスターシェルスクリプトを書くことです。

スクリプトをもっと複雑にする傾向があるので、私は機能を避けようとしています。 (この目的のためにPerlが作成されています)

スクリプトは移植性があることを確認するには#!/ bin/shだけでなく、#!/ bin/ash、#!/ bin/dashなどを使用してテストしてください。すぐにBash固有のコードを見つけることができます。

17

シェルスクリプトは、ファイルやプロセスを操作するための言語です。 これは素晴らしいことですが、一般的な目的の言語ではありませんので、 シェルスクリプトで新しいロジックを再作成するのではなく、常に既存のユーティリティからロジックをグルーミングしようとします。

私はその一般的な原則以外にも、common shell script mistakesを収集しました。

関連する問題