是否有人知道任何有关shell脚本(sh,bash等)的最佳实践或设计模式的资源?
我写了相当复杂的shell脚本,我的第一个建议是"不要"。原因是犯一个小错误很容易阻碍脚本,甚??至使脚本变得危险。
就是说,除了我的个人经历,我没有其他资源可以传递给您。
这是我通常所做的事情,虽然过于冗长,但它已经过时了,但往往很扎实。
调用方式
使您的脚本接受长短选项。请注意,因为有两个命令可以解析选项getopt和getopts。使用getopt可以减少麻烦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| 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等。建议您将它们放入更有意义的名称中。将函数内部的变量声明为局部变量:
1 2 3
| function foo {
local bar="$0"
} |
容易出错的情况
在bash中,除非另行声明,否则将未设置的变量用作空字符串。如果输入错误,这将非常危险,因为将不会报告错误键入的变量,并且该变量将被评估为空。采用
以防止这种情况发生。不过请小心,因为如果这样做,则每次评估未定义的变量时,程序都会中止。因此,检查变量是否未定义的唯一方法是:
1 2 3 4
| if test"x${foo:-notset}" =="xnotset"
then
echo"foo not set"
fi |
您可以将变量声明为只读:
1
| readonly readonly_var="foo" |
模块化
如果使用以下代码,则可以实现"类似于python"的模块化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| 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点。
另外,将其作为模块的第一件事
1 2 3 4 5 6
| # avoid double inclusion
if test"${BashInclude__imported+defined}" =="defined"
then
return 0
fi
BashInclude__imported=1 |
面向对象编程
在bash中,您不能进行面向对象的编程,除非您构建了一个非常复杂的对象分配系统(我认为这是可行的,但是很疯狂)。
但实际上,您可以执行"面向单一编程":每个对象只有一个实例,而只有一个。
我要做的是:我将一个对象定义到一个模块中(请参阅模块化条目)。然后,我定义空的vars(类似于成员变量),一个init函数(构造函数)和成员函数,如本示例代码所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| # 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
} |
诱捕和处理信号
我发现这对于捕获和处理异常很有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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解决。使用另一种语言。由于人为因素和传统,我不得不使用它。
看看Advanced Bash-Scripting Guide,了解有关Shell脚本的很多知识-不仅限于Bash。
不要听别人说要看其他更复杂的语言。如果外壳脚本满足您的需求,请使用该脚本。您需要功能,而不是幻想。新语言为您的简历提供了宝贵的新技能,但是如果您有需要完成的工作并且已经了解Shell,这将无济于事。
如上所述,shell脚本没有很多"最佳实践"或"设计模式"。像其他任何编程语言一样,不同的用法具有不同的准则和偏见。
Shell脚本是一种用于操纵文件和进程的语言。
尽管这样做很不错,但它不是通用语言,
因此,始终尝试从现有实用程序中胶合逻辑,而不是
在shell脚本中重新创建新逻辑。
除了一般原则之外,我还收集了一些常见的Shell脚本错误。
今年(2008年)在OSCON上有一个关于此主题的精彩会议:http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
知道何时使用它。对于快速而肮脏的粘合命令,也可以。如果您需要做出不多的重要决策,循环或其他任何事情,请使用Python,Perl和模块化。
shell的最大问题通常是最终结果看起来像是一个大泥泞球,4000行bash并不断增长……而您无法摆脱它,因为现在您的整个项目都依赖它。当然,它始于40行漂亮的bash。
使用set -e,这样您就不会在出错后继续前进。如果要使其在非Linux上运行,请尝试使其不兼容bash,使其与sh兼容。
简单:
使用python而不是shell脚本。
您可以将可读性提高近100倍,而无需使您不需要的任何事情变得复杂,并且保留了将脚本的各个部分演化为函数,对象,持久性对象(zodb),分布式对象(pyro)的能力,而几乎没有任何复杂性额外的代码。
要找到"最佳做法",请查看Linux发行版(例如Debian)如何编写其初始化脚本(通常在/etc/init.d中找到)
它们中的大多数没有" bash-isms",并且将配置设置,库文件和源格式很好地分开了。
我的个人风格是编写一个定义一些默认变量的master-shellscript,然后尝试加载("源")可能包含新值的配置文件。
我尽量避免使用函数,因为它们会使脚本更加复杂。 (Perl是为此目的而创建的。)
为了确保脚本可移植,不仅可以使用#!/ bin / sh进行测试,还可以使用#!/ bin / ash,#!/ bin / dash等进行测试。您将很快发现Bash特定的代码。
或更旧的报价类似于Joao所说的话:
"使用perl。您将想了解bash但不使用它。"
可悲的是,我忘了是谁说的。
是的,这些天我会推荐在perl上使用python。