有没有人知道任何讨论shell脚本(sh,bash等)的最佳实践或设计模式的资源?
答案 0 :(得分:211)
我编写了相当复杂的shell脚本,我的第一个建议是“不要”。原因是很容易犯一个小错误,妨碍你的脚本,甚至使它变得危险。
那就是说,我没有其他资源可以通过你,但我的个人经历。 这是我通常做的,这是过度的,但往往是坚实的,虽然非常详细。
<强>调用强>
让您的脚本接受长短选项。要小心,因为有两个命令来解析选项,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中调用函数,只需记住在调用之前定义它们。函数类似于脚本,它们只能返回数值。这意味着您必须创建一个不同的策略来返回字符串值。我的策略是使用一个名为RESULT的变量来存储结果,如果函数干净地完成则返回0。 此外,如果返回的值不是零,则可以引发异常,然后设置两个“异常变量”(我的:EXCEPTION和EXCEPTION_MSG),第一个包含异常类型,第二个包含人类可读消息。
当你调用一个函数时,函数的参数被分配给特殊的vars $ 0,$ 1等。我建议你把它们放到更有意义的名字中。将函数内的变量声明为local:
function foo {
local bar="$0"
}
容易出错的情况
在bash中,除非另行声明,否则将unset变量用作空字符串。这在拼写错误的情况下是非常危险的,因为不会报告严重类型的变量,并且它将被评估为空。使用
set -o nounset
防止这种情况发生。但要小心,因为如果这样做,程序将在每次评估未定义变量时中止。因此,检查变量是否未定义的唯一方法如下:
if test "x${foo:-notset}" == "xnotset"
then
echo "foo not set"
fi
您可以将变量声明为readonly:
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中搜索哪些内容。由于您始终在全局命名空间中导入,请记住使用正确的前缀为所有函数和变量添加前缀,否则会冒名字冲突的风险。我使用双下划线作为python点。
此外,将此作为您模块中的第一件事
# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
return 0
fi
BashInclude__imported=1
面向对象编程
在bash中,你不能进行面向对象的编程,除非你构建一个非常复杂的对象分配系统(我想到了。这是可行的,但是很疯狂)。 实际上,您可以执行“面向单例编程”:每个对象只有一个实例,只有一个。
我所做的是:我将一个对象定义为一个模块(参见模块化条目)。然后我定义空变量(类似于成员变量),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解决。用另一种语言。由于人为因素和遗产,我不得不使用它。
答案 1 :(得分:23)
看一下Advanced Bash-Scripting Guide有关shell脚本的很多智慧 - 不仅仅是Bash。
不要听别人告诉你看其他可能更复杂的语言。如果shell脚本满足您的需求,请使用它。你想要功能,而不是想象力。新语言为您的简历提供了宝贵的新技能,但如果您有需要完成的工作并且您已经知道shell,这无济于事。
如上所述,shell脚本没有很多“最佳实践”或“设计模式”。不同的用途有不同的指导和偏见 - 就像任何其他编程语言一样。
答案 2 :(得分:19)
shell脚本是一种用于操作文件和进程的语言。 虽然它很棒,但它不是通用语言, 所以总是尝试从现有的实用程序中粘合逻辑而不是 在shell脚本中重新创建新逻辑。
除了这个一般原则,我收集了一些common shell script mistakes。
答案 3 :(得分:11)
今年(2008年)OSCON在这个主题上有一个很棒的会议:http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
答案 4 :(得分:8)
易于: 使用python而不是shell脚本。 你可以获得近100倍的可读性,而不必使任何你不需要的东西复杂化,并保留将你的部分脚本演变成函数,对象,持久对象(zodb),分布式对象(pyro)的能力,几乎没有任何额外的代码。
答案 5 :(得分:8)
使用set -e,这样你就不会在错误后向前移动。如果你想让它在非Linux上运行,请尝试使它兼容而不依赖于bash。
答案 6 :(得分:8)
知道何时使用它。对于快速和脏粘合命令,它没关系。如果您需要做的不仅仅是几个非平凡的决定,循环,任何事情,请选择Python,Perl和 modularize 。
shell的最大问题通常是最终结果看起来像一个大泥球,4000行bash并且正在增长......你无法摆脱它,因为现在你的整个项目依赖于它。当然,它始于40行美丽的狂欢。
答案 7 :(得分:7)
要找到一些“最佳实践”,请查看Linux发行版(例如Debian)如何编写其init脚本(通常位于/etc/init.d中)
他们中的大多数没有“bash-isms”,并且配置设置,库文件和源格式都很好。
我的个人风格是编写一个master-shellscript来定义一些默认变量,然后尝试加载(“source”)一个可能包含新值的配置文件。
我试图避免使用函数,因为它们会使脚本更复杂。 (Perl是为此目的而创建的。)
为了确保脚本是可移植的,不仅要测试#!/ bin / sh,还要使用#!/ bin / ash,#!/ bin / dash等。你很快就会发现Bash特定代码够了。
答案 8 :(得分:-1)
或类似于Joao所说的旧报价:
“使用perl。你会想知道bash而不是使用它。”
可悲的是,我忘了是谁说的。是的,这些天我会推荐python over perl。