181

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

4

8 に答える 8

232

I wrote quite complex shell scripts and my first suggestion is "don't". The reason is that is fairly easy to make a small mistake that hinders your script, or even make it dangerous.

That said, I don't have other resources to pass you but my personal experience. Here is what I normally do, which is overkill, but tends to be solid, although very verbose.

Invocation

make your script accept long and short options. be careful because there are two commands to parse options, getopt and getopts. Use getopt as you face less trouble.

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

Another important point is that a program should always return zero if completes successfully, non-zero if something went wrong.

Function calls

You can call functions in bash, just remember to define them before the call. Functions are like scripts, they can only return numeric values. This means that you have to invent a different strategy to return string values. My strategy is to use a variable called RESULT to store the result, and returning 0 if the function completed cleanly. Also, you can raise exceptions if you are returning a value different from zero, and then set two "exception variables" (mine: EXCEPTION and EXCEPTION_MSG), the first containing the exception type and the second a human readable message.

When you call a function, the parameters of the function are assigned to the special vars $0, $1 etc. I suggest you to put them into more meaningful names. declare the variables inside the function as local:

function foo {
   local bar="$0"
}

Error prone situations

In bash, unless you declare otherwise, an unset variable is used as an empty string. This is very dangerous in case of typo, as the badly typed variable will not be reported, and it will be evaluated as empty. use

set -o nounset

to prevent this to happen. Be careful though, because if you do this, the program will abort every time you evaluate an undefined variable. For this reason, the only way to check if a variable is not defined is the following:

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

You can declare variables as readonly:

readonly readonly_var="foo"

Modularization

You can achieve "python like" modularization if you use the following code:

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
} 

you can then import files with the extension .shinc with the following syntax

import "AModule/ModuleFile"

Which will be searched in SHELL_LIBRARY_PATH. As you always import in the global namespace, remember to prefix all your functions and variables with a proper prefix, otherwise you risk name clashes. I use double underscore as the python dot.

Also, put this as first thing in your module

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

Object oriented programming

In bash, you cannot do object oriented programming, unless you build a quite complex system of allocation of objects (I thought about that. it's feasible, but insane). In practice, you can however do "Singleton oriented programming": you have one instance of each object, and only one.

What I do is: i define an object into a module (see the modularization entry). Then I define empty vars (analogous to member variables) an init function (constructor) and member functions, like in this example code

# 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
}

Trapping and handling signals

I found this useful to catch and handle exceptions.

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

Hints and tips

If something does not work for some reason, try to reorder the code. Order is important and not always intuitive.

do not even consider working with tcsh. it does not support functions, and it's horrible in general.

Hope it helps, although please note. If you have to use the kind of things I wrote here, it means that your problem is too complex to be solved with shell. use another language. I had to use it due to human factors and legacy.

于 2009-04-10T22:14:48.503 に答える
25

Bashだけでなく、シェル スクリプトに関する多くの知恵については、Advanced Bash-Scripting Guideを参照してください。

もっと複雑な言語に目を向けるように言う人に耳を傾けないでください。シェル スクリプトがニーズを満たしている場合は、それを使用してください。欲しいのは派手さではなく、機能性です。新しい言語は履歴書に役立つ新しいスキルを提供しますが、それは、やらなければならない仕事があり、すでにシェルを知っている場合には役に立ちません。

前述のとおり、シェル スクリプトの「ベスト プラクティス」や「デザイン パターン」はあまりありません。他のプログラミング言語と同様に、用途が異なれば、ガイドラインとバイアスも異なります。

于 2008-09-17T02:44:13.487 に答える
20

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

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

于 2008-09-17T23:57:14.270 に答える
13

いつ使用するかを知っておいてください。すばやく汚れたコマンドを一緒に接着する場合は、問題ありません。自明ではない決定、ループなどをいくつか行う必要がある場合は、Python、Perl、およびモジュール化を使用してください。

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

于 2008-09-17T19:30:13.460 に答える
12

今年 (2008 年) の OSCON では、まさにこのトピックに関する素晴らしいセッションがありました

于 2008-09-17T04:57:28.893 に答える
10

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

于 2008-09-17T00:05:01.840 に答える
9

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

于 2008-09-17T00:02:48.947 に答える
7

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

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

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

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

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

于 2008-09-17T20:33:48.173 に答える