シェルスクリプトの設計パターンまたはベストプラクティス[終了]


167

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


2
昨夜、BASHテンプレートパターンに関する小さな記事を書きました。あなたの考えを見てください。
quickshiftin 2014年

回答:


222

私は非常に複雑なシェルスクリプトを作成しましたが、私の最初の提案は「しないでください」です。その理由は、スクリプトを妨げる小さな間違いを犯したり、それを危険にさらしたりすることはかなり容易だからです。

とは言っても、私には他のリソースはありませんが、私の個人的な経験はありません。これは私が通常行うことです。これはやり過ぎですが、非常に冗長ですが堅実な傾向があります。

呼び出し

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

CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""

getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "$@"`

if test $? != 0
then
    echo "unrecognized option"
    exit 1
fi

eval set -- "$getopt_results"

while true
do
    case "$1" in
        --config_file)
            CommandLineOptions__config_file="$2";
            shift 2;
            ;;
        --debug_level)
            CommandLineOptions__debug_level="$2";
            shift 2;
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "$0: unparseable option $1"
            EXCEPTION=$Main__ParameterException
            EXCEPTION_MSG="unparseable option $1"
            exit 1
            ;;
    esac
done

if test "x$CommandLineOptions__config_file" == "x"
then
    echo "$0: missing config_file parameter"
    EXCEPTION=$Main__ParameterException
    EXCEPTION_MSG="missing config_file parameter"
    exit 1
fi

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

関数呼び出し

bashで関数を呼び出すことができます。呼び出す前にそれらを定義することを忘れないでください。関数はスクリプトのようなもので、数値のみを返すことができます。つまり、文字列値を返すには別の方法を考案する必要があります。私の戦略は、RESULTという変数を使用して結果を保存し、関数が正常に完了した場合は0を返すことです。また、ゼロとは異なる値を返す場合は例外を発生させ、2つの「例外変数」を設定できます(私の場合:EXCEPTIONとEXCEPTION_MSG)。

関数を呼び出すと、関数のパラメーターが特殊な変数$ 0、$ 1などに割り当てられます。より意味のある名前にすることをお勧めします。関数内の変数をローカルとして宣言します。

function foo {
   local bar="$0"
}

エラーが発生しやすい状況

bashでは、特に宣言しない限り、未設定の変数が空の文字列として使用されます。タイプミスのある変数は報告されず、空と評価されるので、タイプミスの場合、これは非常に危険です。使用する

set -o nounset

これを防ぐために。ただし、これを行うと、未定義の変数を評価するたびにプログラムが異常終了するので注意してください。このため、変数が定義されていないかどうかを確認する唯一の方法は次のとおりです。

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

変数を読み取り専用として宣言できます。

readonly readonly_var="foo"

モジュール化

次のコードを使用すると、「Pythonのような」モジュール化を実現できます。

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
} 

次に、拡張子.shincのファイルを次の構文でインポートできます。

「AModule / ModuleFile」をインポート

SHELL_LIBRARY_PATHで検索されます。常にグローバル名前空間にインポートするので、すべての関数と変数の前に適切なプレフィックスを付けることを忘れないでください。そうしないと、名前が衝突する危険があります。pythonドットとして二重下線を使用しています。

また、これをモジュールの最初に置きます

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

オブジェクト指向プログラミング

bashでは、オブジェクト割り当ての非常に複雑なシステムを構築しない限り、オブジェクト指向プログラミングを実行できません(私はそれについて考えました。実現可能ですが、非常識です)。ただし、実際には、「シングルトン指向プログラミング」を実行できます。各オブジェクトのインスタンスは1つで、1つしかありません。

私がしていること:オブジェクトをモジュールに定義します(モジュール化のエントリを参照)。次に、この例のコードのように、空の変数(メンバー変数に類似)、init関数(コンストラクター)、およびメンバー関数を定義します。

# 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 "$@"

ヒントとコツ

何らかの理由で何かが機能しない場合は、コードを並べ替えてみてください。順序は重要であり、常に直感的ではありません。

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

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


7
うわー、私はbashでやり過ぎるつもりだと思っていました...分離された関数を使用したり、サブシェルを悪用したりする傾向があります(したがって、速度が何らかの意味で関係しているときに苦しみます)。内外どちらのグローバル変数もありません(正気の遺跡を維持するため)。すべてがstdoutまたはファイル出力を介して戻ります。set -u / set -e(あまりにも悪いset -eは、最初の場合すぐに役に立たなくなり、ほとんどのコードがそこにあることがよくあります)。[local something = "$ 1";で使用される関数引数。shift](リファクタリング時に簡単に並べ替えることができます)。3000行のbashスクリプトを1つ作成した後、私はこの方法で最小のスクリプトでも書く傾向があります...
ユージーン

モジュール化のための小さな修正:1後はリターンが必要です。「$ script_absolute_dir / $ module.shinc」は、警告の欠落を回避します。2 $ SHELL_LIBRARY_PATHでモジュールを検索して戻る前に、IFS = "$ saved_IFS"を設定する必要があります
Duff

「人的要因」は最悪のものです。機械はあなたがより良いものを与えてもあなたと戦うことはありません。
jeremyjjbrown 2014

1
なぜgetoptgetoptsgetoptsより移植性が高く、あらゆるPOSIXシェルで動作します。特に問題は、特にbashのベストプラクティスではなく、シェルのベストプラクティスであるため、POSIX準拠をサポートして、可能であれば複数のシェルをサポートします。
Wimateeka 2017

1
正直言ってシェルスクリプトに関するすべてのアドバイスを提供してくれてありがとう:「それは役に立ちますが、注意してください。ここで書いたようなものを使用する必要がある場合、問題は複雑すぎて解決できないことを意味しますシェル。別の言語を使用します。人的要因と遺産のため、私はそれを使用する必要がありました。
dieHellste 2017年

25

見てみましょう高度なバッシュ・スクリプトガイドをシェルスクリプトの知恵の多くのために-だけでなく、バッシュ、どちらか。

他の、おそらくもっと複雑な言語を見るように言われる人の言うことを聞かないでください。シェルスクリプトがニーズを満たしている場合は、それを使用してください。あなたは空想ではなく機能性を求めています。新しい言語はあなたの履歴書に貴重な新しいスキルを提供しますが、実行する必要がある作業があり、シェルをすでに知っている場合は、それは役に立ちません。

述べたように、シェルスクリプトの「ベストプラクティス」や「デザインパターン」はそれほど多くありません。他のプログラミング言語と同様に、用途によってガイドラインとバイアスは異なります。


9
少し複雑なスクリプトでも、これはベストプラクティスではないことに注意してください。コーディングとは、何かを機能させることだけではありません。それは、迅速かつ簡単に構築すること、そして信頼性が高く、再利用可能で、読み取りと保守が簡単なことです(特に他の人にとって)。シェルスクリプトは、どのレベルにもうまく拡張できません。ロジックのあるプロジェクトでは、より堅牢な言語の方がはるかに単純です。
ドリフター

20

シェルスクリプトは、ファイルやプロセスを操作するために設計された言語です。それはすばらしいことですが、汎用言語ではないので、シェルスクリプトで新しいロジックを再作成するのではなく、常に既存のユーティリティからロジックを接着するようにしてください。

その一般的な原則以外に、いくつかの一般的なシェルスクリプトの間違いを収集しました。



11

それをいつ使うべきかを知ってください。コマンドをすばやく簡単に接着するには、問題ありません。自明ではない決定、ループ、何かを行う必要がある場合は、Python、Perl、およびmoduleizeを使用してください

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


9

簡単:シェルスクリプトの代わりにPythonを使用します。不要なものを複雑にする必要がなく、スクリプトの一部を関数、オブジェクト、永続オブジェクト(zodb)、分散オブジェクト(pyro)に進化させる機能をほとんど維持することなく、読みやすさがほぼ100倍に向上します。余分なコード。


7
「複雑にする必要がない」と言って、付加価値を与えると思われるさまざまな複雑さをリストすることで矛盾しますが、ほとんどの場合、問題と実装を単純化するために使用されるのではなく、醜いモンスターに悪用されます。
エフゲニー

3
これは大きな欠点を意味し、Pythonが存在しないシステムではスクリプトを移植できません
astropanic

1
これは'08年に回答されたことに気づきました(現在は'12年の2日前です)。ただし、この数年後を見ている人にとっては、PythonやRubyなどの言語が使用可能である可能性が高く、インストールされていない場合はコマンド(または数回のクリック)であるため、PythonやRubyなどの言語に背を向けないように注意してください。 。さらに移植性が必要な場合は、JVMを利用できないマシンを見つけるのに苦労するので、プログラムをJavaで書くことを考えてください。
Wil Moore III

@astropanic今日のほとんどすべてのLinuxポートがPythonに対応
Pithikos 2017年

@Pithikos、確かに、そしてpython2とpython3の面倒をいじる。今日、私はすべてのツールをgoで書いており、幸せになることはできません。
アストロパニックの2017年

9

エラーの後に前進しないように、set -eを使用してください。not-linuxで実行したい場合は、bashに依存せずにsh互換にしてみてください。


7

いくつかの「ベストプラクティス」を見つけるには、Linuxディストリビューション(Debianなど)がinit-scripts(通常は/etc/init.dにあります)をどのように書くかを見てください。

それらのほとんどは "bash-isms"がなく、構成設定、ライブラリファイル、およびソースフォーマットが適切に分離されています。

私の個人的なスタイルは、いくつかのデフォルト変数を定義するマスターシェルスクリプトを記述してから、新しい値を含む可能性のある構成ファイルをロード(「ソース」)しようとします。

関数はスクリプトをより複雑にする傾向があるため、関数は避けます。(Perlはその目的のために作成されました。)

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


-1

または、ジョアンが言ったことに似た古い引用:

「perlを使用してください。bashについては知りたいが、使用しない方がよいでしょう。」

悲しいことに、誰が言ったか忘れてしまいました。

そして、はい、最近はperlよりもpythonをお勧めします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.