Shell 脚本的设计模式或最佳实践

有谁知道有什么资源讨论 shell 脚本(sh、 bash 等)的最佳实践或设计模式吗?

75518 次浏览

简单: 使用 python 代替 shell 脚本。 你的可读性提高了近100倍,而且不需要复杂化任何你不需要的东西,并且保留了将脚本的某些部分演化成函数、对象、持久对象(zodb)、分布式对象(pyro)的能力,几乎不需要任何额外的代码。

使用 set-e,这样你就不会在错误之后继续前进。如果您想让它在 not-linux 上运行,请尝试在不依赖 bash 的情况下使它兼容 sh。

高级 Bash 脚本指南中可以找到许多关于 shell 脚本编程的智慧——不仅仅是 Bash。

不要听信别人告诉你去看看其他更复杂的语言。如果 shell 脚本满足您的需要,请使用它。你想要的是功能性,而不是花哨。新的语言为你的简历提供了有价值的新技能,但是如果你有工作要做,而且你已经知道 shell,那么这些都没有帮助。

如前所述,对于 shell 脚本来说,并没有很多“最佳实践”或“设计模式”。不同的用法有不同的指导方针和偏见——就像任何其他编程语言一样。

今年(2008年)在 OSCON 有一个很棒的会议,就是关于这个主题: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf

知道什么时候该用。对于快速和肮脏的粘合命令,可以使用。如果您需要做一些非平凡的决策、循环或其他任何事情,那么可以使用 Python、 Perl 和 模块化

Shell 最大的问题往往是最终的结果看起来就像一个大泥球,有4000行重击和生长... ... 你不能摆脱它,因为现在你的整个项目都依赖于它。当然,从40行开始的美丽的 bash。

要找到一些“最佳实践”,请看 Linux 发行版(例如 Debian)如何编写初始化脚本(通常在/etc/init.d 中找到)

它们中的大多数都没有“ bash-ism”,并且很好地分离了配置设置、库文件和源代码格式。

我的个人风格是编写一个 master-shell 脚本,定义一些默认变量,然后尝试加载(“ source”)一个可能包含新值的配置文件。

我尽量避免使用函数,因为它们往往会使脚本更加复杂(创建 Perl 就是为了这个目的)

为了确保脚本是可移植的,不仅要用 # 来测试!/bin/sh,还可以使用 # !# 垃圾桶/灰烬 # !/垃圾桶/破折号等。你很快就会发现 Bash 的特定代码。

Shell 脚本是一种用于操作文件和进程的语言。 虽然这很好,但它不是一种通用语言, 所以总是尝试从现有的实用程序粘合逻辑,而不是 在 shell 脚本中重新创建新的逻辑。

除了这个基本原理,我还收集了一些 常见的 shell 脚本错误

我编写了相当复杂的 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 T 的变量来存储结果,如果函数完全完成,则返回0。 另外,如果返回的值与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_var="foo"

模块化

如果使用以下代码,您可以实现“ Python 样”模块化:

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 中,您不能进行面向对象的编程,除非您构建一个相当复杂的对象分配系统(我考虑过这个问题)。这是可行的,但是很疯狂)。 实际上,您可以执行“面向单例的编程”: 每个对象都有一个实例,而且只有一个。

我要做的是: 将一个对象定义到一个模块中(参见模块化条目)。然后定义空 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 来解决。用另一种语言。由于人类的因素和遗产,我不得不使用它。