シェル スクリプトのデザイン パターンまたはベスト プラクティス [終了]
-
09-06-2019 - |
質問
シェル スクリプト (sh、bash など) のベスト プラクティスや設計パターンについて説明しているリソースを知っている人はいますか?
解決
私は非常に複雑なシェル スクリプトを作成しましたが、最初の提案は「やめてください」です。その理由は、スクリプトを妨げたり、スクリプトを危険にしたりする小さな間違いを非常に簡単に犯してしまうためです。
とはいえ、私の個人的な経験以外にお伝えできるリソースはありません。これは私が普段やっていることです。やり過ぎですが、堅実になりがちですが、 とても 冗長。
呼び出し
スクリプトが長いオプションと短いオプションを受け入れるようにします。オプションを解析するには getopt と getopts という 2 つのコマンドがあるので注意してください。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 つの重要な点は、プログラムは正常に完了した場合は常に 0 を返し、何か問題が発生した場合は 0 以外を返す必要があるということです。
関数呼び出し
bash で関数を呼び出すことができますが、呼び出す前に関数を定義することを忘れないでください。関数はスクリプトのようなもので、数値のみを返すことができます。これは、文字列値を返すために別の戦略を考案する必要があることを意味します。私の戦略は、RESULT という変数を使用して結果を保存し、関数が正常に完了した場合は 0 を返すことです。また、ゼロ以外の値を返す場合に例外を発生させ、2 つの「例外変数」を設定することもできます (私の場合:EXCEPTION および EXCEPTION_MSG)、1 番目には例外タイプが含まれ、2 番目には人間が判読できるメッセージが含まれます。
関数を呼び出すと、関数のパラメータは特殊な変数 $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 を持つファイルをインポートできます。
import "AModule/ModuleFile"
これは SHELL_LIBRARY_PATH で検索されます。常にグローバル名前空間にインポートするため、すべての関数と変数に適切な接頭辞を付けることを忘れないでください。そうしないと、名前が衝突する危険があります。Python のドットとして二重アンダースコアを使用します。
また、これをモジュールの最初に入れてください
# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
return 0
fi
BashInclude__imported=1
オブジェクト指向プログラミング
bash では、オブジェクトを割り当てる非常に複雑なシステムを構築しない限り、オブジェクト指向プログラミングを行うことはできません (私はそれについて考えました。実現可能ですが、非常識です)。ただし、実際には、「シングルトン指向プログラミング」を実行できます。各オブジェクトのインスタンスは 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 を使用することさえ考慮しないでください。関数はサポートされておらず、全体的にひどいものです。
参考になれば幸いですが、ご了承ください。ここで私が書いたようなものを使用しなければならない場合、それは問題が複雑すぎてシェルで解決できないことを意味します。別の言語を使用してください。人的要因と遺産のため、これを使用する必要がありました。
他のヒント
を見てください。 高度な Bash スクリプト ガイド Bash だけでなく、シェル スクリプトに関する多くの知恵にも感謝します。
他の、おそらくより複雑な言語を検討するように人々が言うのに耳を傾けないでください。シェル スクリプトがニーズを満たす場合は、それを使用してください。欲しいのは派手さではなく機能性です。新しい言語は履歴書に貴重な新しいスキルを提供しますが、やらなければならない仕事があり、すでにシェルを知っている場合は役に立ちません。
前述したように、シェル スクリプトには「ベスト プラクティス」や「デザイン パターン」はあまりありません。他のプログラミング言語と同様に、使用法が異なれば、ガイドラインやバイアスも異なります。
シェルスクリプトは、ファイルやプロセスを操作するために設計された言語です。それは素晴らしいことですが、それは汎用言語ではないので、シェルスクリプトで新しいロジックを再現するのではなく、常に既存のユーティリティからロジックを接着してみてください。
その一般原則以外にもいくつか集めてみました よくあるシェルスクリプトの間違い.
今年 (2008 年) の OSCON では、まさにこのトピックに関する素晴らしいセッションがありました。 http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
簡単:シェルスクリプトの代わりにPythonを使用してください。必要のないものを複雑にすることなく、スクリプトの一部を関数、オブジェクト、永続オブジェクト (zodb)、分散オブジェクト (pyro) に進化させる機能をほぼそのまま維持しながら、可読性が 100 倍近く向上します。追加のコード。
set -e を使用すると、エラー後に先へ進むことがなくなります。Linux 以外で実行したい場合は、bash に依存せずに sh 互換にしてみてください。
いつ使用するかを知ってください。 コマンドを素早く貼り付けても大丈夫です。ループなど、重要な決定を行う必要がある場合は、Python、Perl、および モジュール化する.
シェルの最大の問題は、多くの場合、最終結果が 4,000 行の bash と成長する大きな泥の塊のように見えることです...プロジェクト全体がそれに依存しているため、それを取り除くことはできません。もちろん、 40行から始まりました 美しいバッシュの。
いくつかの「ベスト プラクティス」を見つけるには、Linux ディストリビューション (例:Debian) の init スクリプトを作成します (通常は /etc/init.d にあります)
それらのほとんどには「bash-ism」がなく、構成設定、ライブラリファイル、ソースフォーマットが適切に分離されています。
私の個人的なスタイルは、いくつかのデフォルト変数を定義するマスターシェルスクリプトを作成し、新しい値を含む可能性のある構成ファイルをロード (「ソース」) しようとすることです。
関数はスクリプトを複雑にする傾向があるため、関数は避けるようにしています。(Perl はその目的のために作成されました。)
スクリプトが移植可能であることを確認するには、#!/bin/sh だけでなく、#!/bin/ash、#!/bin/dash なども使用してテストします。Bash 固有のコードはすぐに見つかります。
または、ジョアンの言葉に似た古い引用:
「パールを使ってください。bash について知りたいと思うかもしれませんが、使用することはできません。」
残念ながら誰が言ったか忘れてしまいました。
そして、はい、最近では Perl よりも Python をお勧めします。