bash shell 脚本的设计模式或最佳实践
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/78497/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Design patterns or best practices for shell scripts
提问by user14437
Does anyone know of any resources that talk about best practices or design patterns for shell scripts (sh, bash etc.)?
有谁知道关于 shell 脚本(sh、bash 等)的最佳实践或设计模式的任何资源?
回答by Stefano Borini
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.
我写了相当复杂的 shell 脚本,我的第一个建议是“不要”。原因是很容易犯一个妨碍脚本的小错误,甚至使它变得危险。
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 veryverbose.
也就是说,我没有其他资源可以通过你,但我的个人经验。这是我通常做的,这是矫枉过正,但往往是可靠的,虽然非常冗长。
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.
让您的脚本接受多头和空头选项。请注意,因为有两个命令可以解析选项,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 "" in
--config_file)
CommandLineOptions__config_file="";
shift 2;
;;
--debug_level)
CommandLineOptions__debug_level="";
shift 2;
;;
--)
shift
break
;;
*)
echo "function foo {
local bar="set -o nounset
"
}
: unparseable option "
EXCEPTION=$Main__ParameterException
EXCEPTION_MSG="unparseable option "
exit 1
;;
esac
done
if test "x$CommandLineOptions__config_file" == "x"
then
echo "if test "x${foo:-notset}" == "xnotset"
then
echo "foo not set"
fi
: 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.
您可以在 bash 中调用函数,只需记住在调用之前定义它们。函数就像脚本,它们只能返回数值。这意味着您必须发明一种不同的策略来返回字符串值。我的策略是使用一个名为 RESULT 的变量来存储结果,如果函数完全完成则返回 0。此外,如果您返回一个不同于零的值,您可以引发异常,然后设置两个“异常变量”(我的:EXCEPTION 和 EXCEPTION_MSG),第一个包含异常类型,第二个包含人类可读的消息。
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:
当你调用一个函数时,函数的参数被分配给特殊的变量 $0、$1 等。我建议你把它们放在更有意义的名字中。将函数内的变量声明为局部变量:
readonly readonly_var="foo"
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
在 bash 中,除非另外声明,否则未设置的变量将用作空字符串。这在拼写错误的情况下是非常危险的,因为错误类型的变量不会被报告,它会被评估为空。用
set -o nounset
function getScriptAbsoluteDir {
# @description used to get the script path
# @param the script # avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
return 0
fi
BashInclude__imported=1
parameter
local script_invoke_path=""
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="# 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 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=""
# 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 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=
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
}
"
script_name=`basename "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 "$@"
"`
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 the .shinc file to import, without .shinc extension
module=
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
}
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:
以防止这种情况发生。但是要小心,因为如果你这样做,程序将在你每次评估一个未定义的变量时中止。因此,检查变量是否未定义的唯一方法如下:
##代码##You can declare variables as readonly:
您可以将变量声明为只读:
##代码##Modularization
模块化
You can achieve "python like" modularization if you use the following code:
如果使用以下代码,则可以实现“类似python”的模块化:
##代码##you can then import files with the extension .shinc with the following syntax
然后,您可以使用以下语法导入扩展名为 .shinc 的文件
import "AModule/ModuleFile"
导入“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.
将在 SHELL_LIBRARY_PATH 中搜索。由于您总是在全局命名空间中导入,请记住使用适当的前缀为所有函数和变量添加前缀,否则您将面临名称冲突的风险。我使用双下划线作为 python 点。
Also, put this as first thing in your module
另外,把它作为你模块中的第一件事
##代码##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.
在 bash 中,您不能进行面向对象编程,除非您构建了一个相当复杂的对象分配系统(我想过。这是可行的,但很疯狂)。实际上,您可以进行“面向单例编程”:每个对象都有一个实例,并且只有一个实例。
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
我所做的是:我将一个对象定义到一个模块中(请参阅模块化条目)。然后我定义了空变量(类似于成员变量)一个 init 函数(构造函数)和成员函数,就像在这个示例代码中一样
##代码##Trapping and handling signals
捕获和处理信号
I found this useful to catch and handle exceptions.
我发现这对于捕获和处理异常很有用。
##代码##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.
甚至不考虑使用 tcsh。它不支持功能,而且总体上很糟糕。
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.
希望它有所帮助,但请注意。如果非得用我这里写的那种东西,说明你的问题太复杂,用shell解决不了。使用另一种语言。由于人为因素和遗产,我不得不使用它。
回答by jtimberman
Take a look at the Advanced Bash-Scripting Guidefor a lot of wisdom on shell scripting - not just Bash, either.
查看高级 Bash 脚本指南,了解有关 shell 脚本的很多智慧——不仅仅是 Bash。
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.
不要听别人告诉你看其他的,可以说是更复杂的语言。如果 shell 脚本满足您的需要,请使用它。你想要功能,而不是花哨。新语言为您的简历提供了宝贵的新技能,但如果您有需要完成的工作并且您已经了解 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 脚本没有很多“最佳实践”或“设计模式”。不同的用途有不同的指导方针和偏见——就像任何其他编程语言一样。
回答by pixelbeat
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.
shell 脚本是一种旨在操作文件和进程的语言。虽然这很好,但它不是一种通用语言,所以总是尝试从现有实用程序中粘合逻辑,而不是在 shell 脚本中重新创建新逻辑。
Other than that general principle I've collected some common shell script mistakes.
除了一般原则之外,我还收集了一些常见的 shell 脚本错误。
回答by Fhoxh
There was a great session at OSCON this year (2008) on just this topic: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
今年(2008 年)在 OSCON 上有一个关于这个主题的精彩会议:http: //assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
回答by Pawe? Hajdan
Know when to use it.For quick and dirty gluing commands together it's okay. If you need to make any more than few non-trivial decisions, loops, anything, go for Python, Perl, and modularize.
知道何时使用它。对于快速而肮脏的粘合命令,没关系。如果您需要做出一些重要的决定、循环等任何事情,请选择 Python、Perl 和模块化。
The biggest problem with shell is often that end result just looks like a big ball of mud, 4000 lines of bash and growing... and you can't get rid of it because now your whole project depends on it. Of course, it started at 40 linesof beautiful bash.
shell 的最大问题通常是最终结果看起来就像一个大泥球,4000 行 bash 并且还在增长……而且您无法摆脱它,因为现在您的整个项目都依赖于它。当然,它从 40 行漂亮的 bash 开始。
回答by user10392
use set -e so you don't plow forward after errors. Try making it sh compatible without relying on bash if you want it to run on not-linux.
使用 set -e 这样你就不会在错误后继续前进。如果您希望它在非 Linux 上运行,请尝试使其与 sh 兼容,而不依赖于 bash。
回答by user10392
Easy: use python instead of shell scripts. You get a near 100 fold increase in readablility, without having to complicate anything you don't need, and preserving the ability to evolve parts of your script into functions, objects, persistent objects (zodb), distributed objects (pyro) nearly without any extra code.
简单:使用 python 而不是 shell 脚本。您的可读性提高了近 100 倍,而不必使您不需要的任何东西复杂化,并且保留了将脚本的一部分演变为函数、对象、持久对象 (zodb)、分布式对象 (pyro) 的能力,几乎没有任何额外的代码。
回答by Willem
To find some "best practices", look how Linux distro's (e.g. Debian) write their init-scripts (usually found in /etc/init.d)
要找到一些“最佳实践”,请查看 Linux 发行版(例如 Debian)如何编写他们的 init 脚本(通常在 /etc/init.d 中找到)
Most of them are without "bash-isms" and have a good separation of configuration settings, library-files and source formatting.
它们中的大多数都没有“bash-isms”,并且很好地分离了配置设置、库文件和源格式。
My personal style is to write a master-shellscript which defines some default variables, and then tries to load ("source") a configuration file which may contain new values.
我的个人风格是编写一个定义一些默认变量的 master-shellscript,然后尝试加载(“源”)一个可能包含新值的配置文件。
I try to avoid functions since they tend to make the script more complicated. (Perl was created for that purpose.)
我尽量避免使用函数,因为它们往往会使脚本变得更加复杂。(Perl 就是为此目的而创建的。)
To make sure the script is portable, test not only with #!/bin/sh, but also use #!/bin/ash, #!/bin/dash, etc. You'll spot the Bash specific code soon enough.
为了确保脚本是可移植的,不仅要使用 #!/bin/sh 进行测试,还要使用 #!/bin/ash、#!/bin/dash 等。您很快就会发现 Bash 特定代码。
回答by Sarien
Or the older quote similar to what Joao said:
或者类似于Joao所说的旧引用:
"Use perl. You will want to know bash but not use it."
“使用 perl。你会想知道 bash 但不会使用它。”
Sadly I forgot who said that.
可惜我忘了这是谁说的。
And yes these days I would recommend python over perl.
是的,这些天我会推荐 python 而不是 perl。