问题描述
有人知道有什么资源谈论有关外壳脚本的最佳实践或设计模式(sh,bash等)?
推荐答案
我编写了相当复杂的外壳脚本,我的第一个建议是"不要".原因是犯一个小错误以阻碍您的脚本,甚至使其变得危险相当容易.
也就是说,我没有其他资源可以通过您,而是我的个人经验. 这是我通常所做的,这是过度的,但往往是坚固的,尽管非常冗长.
invocation
让您的脚本接受长时间和简短的选择.要小心,因为有两个命令可以解析选项,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
另一个重要的一点是,如果成功完成,则程序应始终返回零,如果出现问题,则不零.
功能调用
您可以在bash中调用功能,只需记住在呼叫之前定义它们即可.函数就像脚本一样,它们只能返回数字值.这意味着您必须发明不同的策略才能返回字符串值.我的策略是使用称为结果的变量存储结果,如果功能干净完成,则返回0. 另外,如果要返回与零不同的值,则可以提出异常,然后设置两个"异常变量"(mine:exception和exception_msg),第一个包含异常类型,第二个包含人类可读消息.
.调用函数时,该函数的参数将分配给特殊vars $ 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"
模块化
如果使用以下代码:
,则可以实现"像"模块化.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 }
然后,您可以使用以下语法导入Extension .shinc
导入" amodule/modulefile"
将在shell_library_path中搜索.当您始终在全局名称空间中导入时,请记住将所有功能和变量以适当的前缀为前缀,否则您会冒险冲突.我将双重下划线用作python点.
另外,将其作为模块中的第一件事
# avoid double inclusion if test "${BashInclude__imported+defined}" == "defined" then return 0 fi BashInclude__imported=1
面向对象的编程
在bash中,除非您建立一个非常复杂的对象分配系统(我考虑到这一点.这是可行的,但疯了). 但是,实际上,您可以执行"面向单身的编程":每个对象的一个实例,只有一个.
我要做的是:我将对象定义为模块(请参阅模块化条目).然后,我定义空vars(类似于成员变量)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合作.它不支持功能,而且总体上是可怕的.
希望它有帮助,尽管请注意.如果您必须使用我在这里写的东西,这意味着您的问题太复杂了,无法用Shell解决.使用另一种语言.由于人为因素和遗产,我必须使用它.
其他推荐答案
看一下高级bash-scripting指南在外壳脚本上有很多智慧 - 不仅仅是狂欢.
不要听别人告诉你看别人,可以说更复杂的语言.如果外壳脚本满足您的需求,请使用它.您想要功能,而不是幻想.新语言为您的简历提供了宝贵的新技能,但是如果您有需要完成的工作并且您已经知道Shell,这无济于事.
如前所述,没有很多用于外壳脚本的"最佳实践"或"设计模式".不同的用途具有不同的准则和偏见 - 像其他任何编程语言一样.
其他推荐答案
Shell脚本是一种旨在操纵文件和过程的语言. 虽然这很棒,但它不是一种通用语言,但 因此,始终尝试从现有实用程序中粘合逻辑,而不是 在shell脚本中重新创建新逻辑.
除了这一一般原则外,我还收集了一些html" rel="noreferry="noreferrer" rel="noreferrer" title="common sell脚本错误.
问题描述
Does anyone know of any resources that talk about best practices or design patterns for shell scripts (sh, bash etc.)?
推荐答案
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.
其他推荐答案
Take a look at the Advanced Bash-Scripting Guide for a lot of wisdom on shell scripting - not just Bash, either.
Don't listen to people telling you to look at other, arguably more complex languages. If shell scripting meets your needs, use that. You want functionality, not fanciness. New languages provide valuable new skills for your resume, but that doesn't help if you have work that needs to be done and you already know shell.
As stated, there aren't a lot of "best practices" or "design patterns" for shell scripting. Different uses have different guidelines and bias - like any other programming language.
其他推荐答案
shell script is a language designed to manipulate files and processes. While it's great for that, it's not a general purpose language, so always try to glue logic from existing utilities rather than recreating new logic in shell script.
Other than that general principle I've collected some common shell script mistakes.