shell脚本的devise模式或最佳实践
有谁知道任何资源谈论的最佳做法或devise模式的shell脚本(sh,bash等)?
我写了相当复杂的shell脚本,我的第一个build议是“不”。 原因在于制作一个小小的错误是很容易的,这个错误会阻碍你的剧本,甚至使之变得危险。
也就是说,我没有其他资源可以通过你,但我的个人经验。 这是我通常做的,这是过度杀伤,但往往是坚实的,虽然非常冗长。
调用
让你的脚本接受多空的select。 要小心,因为有两个命令来parsing选项,getopt和getopts。 使用getopt,因为你面对更less的麻烦。
CommandLineOptions__config_file="" CommandLineOptions__debug_level="" getopt_results=`getopt -s bash -oc: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
另一个重要的一点是,一个程序应该总是返回零,如果成功完成,非零,如果出了问题。
函数调用
你可以在bash中调用函数,只要记得在调用之前定义它们。 函数就像脚本,它们只能返回数值。 这意味着你必须发明一个不同的策略来返回string值。 我的策略是使用名为RESULT的variables来存储结果,如果函数完全干净地返回0。 此外,如果您返回的值不是零,则可以引发exception,然后设置两个“exceptionvariables”(mine:EXCEPTION和EXCEPTION_MSG),第一个包含exceptiontypes,第二个包含人类可读的消息。
当你调用一个函数时,函数的参数被分配给特殊的variables$ 0,$ 1等。我build议你把它们放到更有意义的名字中。 在函数内声明variables为local:
function foo { local bar="$0" }
容易出错的情况
在bash中,除非另外声明,否则unsetvariables将用作空string。 如果input错误,这是非常危险的,因为错误的typesvariables不会被报告,并且将被评估为空。 使用
set -o nounset
以防止这种情况发生。 要小心,因为如果你这样做,程序会在你每次评估一个未定义的variables时中止。 出于这个原因,检查一个variables是否没有定义的唯一方法如下:
if test "x${foo:-notset}" == "xnotset" then echo "foo not set" fi
你可以声明variables为只读:
readonly readonly_var="foo"
模块化
如果您使用以下代码,则可以实现“python like”模块化:
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中进行search。 由于您始终在全局名称空间中导入,因此请记住以正确的前缀为所有函数和variables加上前缀,否则可能会导致名称冲突。 我使用双下划线作为python点。
另外,把它作为你的模块的第一件事
# avoid double inclusion if test "${BashInclude__imported+defined}" == "defined" then return 0 fi BashInclude__imported=1
面向对象编程
在bash中,除非你构build了一个相当复杂的对象分配系统(我想过这个,这是可行的,但是疯狂的),你不能做面向对象的编程。 实际上,你可以做“Singleton oriented programming”:每个对象只有一个实例,只有一个实例。
我所做的是:我将一个对象定义到一个模块中(请参阅模块化条目)。 然后我定义了空variables(类似于成员variables),一个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 }
捕获和处理信号
我发现这有助于捕捉和处理exception。
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 "$@"
提示和技巧
如果出于某种原因无法正常工作,请尝试对代码重新sorting。 订单很重要,并不总是直观。
甚至不考虑使用tcsh。 它不支持函数,总的来说是可怕的。
希望它有帮助,但请注意。 如果你不得不使用我在这里写的东西,这意味着你的问题太复杂了,不能用shell来解决。 使用另一种语言。 由于人为因素和遗留问题,我不得不使用它。
看一下高级Bash脚本指南在shell脚本上有很多的智慧 – 不仅仅是Bash。
不要听别人告诉你看其他的,可以说是更复杂的语言。 如果shell脚本符合您的需求,请使用它。 你想要的function,而不是幻想。 新的语言为你的简历提供了有价值的新技能,但是如果你有需要完成的工作,而且你已经知道了shell,这并没有什么帮助。
如上所述,shell脚本没有太多的“最佳实践”或“devise模式”。 不同的用途有不同的指导方针和偏见 – 像任何其他编程语言
shell脚本是一种旨在操作文件和进程的语言。 虽然这很好,但它不是一种通用的语言,所以总是尝试从现有的实用程序中粘合逻辑,而不是在shell脚本中重新创build新的逻辑。
除了这个一般原则,我收集了一些常见的shell脚本错误 。
OSCON今年(2008年)在这个主题上举行了一个很棒的会议: http : //assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
简单:使用python而不是shell脚本。 可读性提高了近100倍,而不必复杂化任何不需要的任何东西,并且保留了将部分脚本演化为函数,对象,持久对象(zodb),分布式对象(pyro)几乎没有任何额外的代码。
使用set -e,所以你不会在错误之后继续前进。 如果你想让它在not-linux上运行,不要依赖bash,尽量让sh兼容。
知道何时使用它。 对于快速和肮脏的粘合命令在一起没关系。 如果你需要进行更多的非平凡的决定,循环,任何事情,那就去Python,Perl和模块化 。
shell的最大问题往往是最终的结果就像是一大块泥巴,4000线的bash,并且越来越多…而你无法摆脱它,因为现在你的整个项目都依赖于它。 当然, 它开始于40行美丽的bash。
要find一些“最佳实践”,看看Linux发行版(例如Debian)如何编写其init脚本(通常在/etc/init.d中find)
他们大多没有“bash-isms”,并且configuration设置,库文件和源格式有很好的分离。
我的个人风格是编写一个定义了一些默认variables的master-shellscript,然后尝试加载(“源”)一个可能包含新值的configuration文件。
我试图避免function,因为他们倾向于使脚本更复杂。 (Perl是为此目的而创build的。)
为了确保脚本是可移植的,不仅要使用#!/ bin / sh来testing,而且还要使用#!/ bin / ash,#!/ bin / dash等等。你会很快发现Bash特定的代码。
或者类似于Joao所说的那句老话:
“使用Perl,你会想知道bash,但不使用它。”
可惜我忘了是谁说的。
是的,这些天我会推荐python over perl。