说明
shell语言有些特点,返回值是0表示当前shell命令执行成功,返回值非零大多数情况下表示当前shell命令执行出现了非预期的情况。同时当shell脚本出现这种非预期情况时,默认情况下shell会继续执行下一行代码。
问题场景
例如:
$ ls
script.sh
$ cat script.sh
#!/bin/bash
cp dev.conf prod.conf
echo 'this is a line configuration' >> actual.conf
$ ./script.sh
cp: cannot stat ‘dev.conf’: No such file or directory
$ ls
prod.conf script.sh
$ cat prod.conf
this is a line configuration
以上代码中,如果由于某种原因(如rpm包的安装)dev.conf 文件没有创建,那么cp命令就是返回非零码。shell仍然继续执行下面一行重定向命令。
生产环境中,就曾遇到如下一些问题:
1)机房由于网络环境配置问题,导致配置模板中的添加默认路由的shell函数 add_ali_public_net_route 执行失败,即返回非零。这个模板在实验室已经测试通过,并且在其他所有机房都装机正常。
2)千岛湖机房装机中会存在一定比例的yum装包不成功的问题,分析发现主要是千岛湖机房的yum源不稳定。这个模板也在实验室已经测试通过,并且在大部分机房都装机正常。
针对shell语言出在的以上问题,目前同事们有如下一些解决方案:
1)在run.sh中调另外一个run.py的python脚本,利用python这种高级语言的特性实现代码的可靠性。
2)在shell脚本的可能出问题的地方,增加条件判断。如下代码,会让代码量成倍增加。即使封装函数后,也会使代码增加不少。
df -h | grep -Ew '/home/admin'
if [ $?-ne 0 ]; then
echo "required dir not mounted"
exit 1
fi
3)在check.sh增加检测项。出rpm包的检测项具备相对可操作性之外,其他很多地方不具可操作性。
shell信号捕获技术介绍
下面是一行捕获shell命令中返回非0返回码的代码。从这个定义行之后的行开始,每行命令执行后返回非0码会触发。触发后,将执行中间的echo输出。
trap 'echo "This shell command returns no 0 code"' ERR
以下是一个具体的例子
$ cat common.sh
#!/bin/bash
trap 'status=$?;echo "script $BASH_SOURCE is error";exit $status' ERR
$ cat file1.sh
#!/bin/bash
source $(dirname $0)/common.sh
./file2.sh
$ cat file2.sh
#!/bin/bash
source $(dirname $0)/common.sh
./file3.sh
$ cat file3.sh
#!/bin/bash
source $(dirname $0)/common.sh
ls file_is_not_exist
echo 'this line command will not be executed'
$./file1.sh
ls: cannot access file_is_not_exist: No such file or directory
script ./file3.sh is error
script ./file2.sh is error
script ./file1.sh is error
运行file1.sh脚本,file1.sh调用file2.sh,file2.sh调用file3.sh。file3.sh中会执行ls,显示一个不存在文件file_is_not_exist,此时ls file_is_not_exist 会返回一个非0返回值,触发 trap … ERR的信号捕获。进一步打印出了一个类似其他高级语言的错误栈的信息。并且,其后语句echo 'this line command will not be executed’将永远不会被执行,确保了脚本的严谨性和可靠性。
shell捕获技术除了可以捕获ERR伪信号之外,还可以捕获EXIT伪信号。下面是一行捕获shell整个脚本执行结束的代码。定义之后的。触发后,将执行中间的echo输出。
trap 'echo "there is the exit of shell script"' EXIT
使用shell信号捕获技术的实例介绍
以下是一个名为script_example的配置模板的目录结构,模板根目录是script_example 。其中clone.json和NEW_JSON两个文件是克隆模板核心分区部分,不在本文的说明范围。config.ini和common.sh文件是一些全局配置和函数定义部分。run.sh和post-install目录是分区后的更个性化的装机逻辑部分,其中post-install目录下的文件(例如initialize_directory.sh set_system_configuration.sh upgrade_kernel.sh)是对run.sh的展开。
$ cd clone_script_example/
$ ls
clone.json check.sh common.sh config.ini NEW_JSON post-install run.sh
$ ls post-install/
initialize_directory.sh set_system_configuration.sh upgrade_kernel.sh
config.ini文件定义了一些全局配置的信息,包括模板名称、post-install文件与执行顺序、其他一些用户自定义全局变量。
$ cat config.ini
# base param
clone_template='clone_script_example' # 克隆模板名称这里定义
scriptsdir="/usr/local/clonescripts/${clone_template}/post-install"
# post-install script list
postinstall_scripts='initialize_directory.sh upgrade_kernel.sh set_system_configuration.sh'
# user defined param
lib_path_list='/usr/ali/bin /usr/ali/sbin /usr/ali/lib /usr/ali/lib64 /usr/ali/include'
# this is other user defined param
# other_vars=.......
functions.sh是系统的通用函数的函数定义文件,source进来可以在用户自定义的克隆模板中使用这些通用函数。
set -o errtrace 命令可以让 trap ‘command’ ERR信号捕获命令在当前生效的页面中的函数、子shell和命令替换中继承生效。
在shell的各种内置变量中,有如下几个在这里特别有用。
BASHSOURCE等价于 B A S H S O U R C E 等 价 于 BASH_SOURCE[0]表示执行命令的本脚本,BASHSOURCE[1]表示调用本脚本的脚本, B A S H S O U R C E [ 1 ] 表 示 调 用 本 脚 本 的 脚 本 , BASH_SOURCE[2]依次类推;LINENO表示代码在脚本中的实际行数位置; L I N E N O 表 示 代 码 在 脚 本 中 的 实 际 行 数 位 置 ; BASH_COMMAND表示当前执行的命令本身。
结合以上shell内置变量,可以将trap ‘command’ ERR语句完善的更加易用,当捕获到ERR信号后,打印出更加详细的信息。
$ cat common.sh
#!/bin/bash
source /usr/local/clonescripts/public/functions.sh
set -o errtrace
trap 'status=$?;echo "EXCEPTION @SCRIPT:$BASH_SOURCE,LINE:$LINENO; COMMAND:$BASH_COMMAND; STATUS:$status";exit $status' ERR
# Customed functions are defined:
function runmodule() {
modulename=$1
[ -f ${stagedir}/$modulename ] && chmod a+x ${scriptsdir}/$modulename
echo "[RUN MODULE $1]"
${scriptsdir}/$modulename
echo "[FINISH MODULE $1]"
}
脚本中定义了一个log_exit_status函数,利用run.sh脚本退出时的伪信号EXIT的捕获,触发这个函数,从而将退出状态(正常或异常)保存到/tmp/clone_run_exit_status这个临时文件中。
$ cat run.sh
#!/bin/bash
trap 'exit_status=$?;log_exit_status $exit_status' EXIT
source $(dirname $0)/config.ini
source $(dirname $0)/common.sh
function log_exit_status(){
exit_status=$1
echo $exit_status > /tmp/clone_run_exit_status
}
function main() {
chmod a+x $0
for s in `echo $postinstall_scripts`
do
runmodule $s
done
}
main
initialize_directory.sh脚本负责初始化一些额外需要创建的目录。
$cat post-install/initialize_directory.sh
#!/bin/bash
source $(dirname $0)/../config.ini
source $(dirname $0)/../common.sh
function main(){
for dir in `echo $lib_path_list`;do
[ -d ${dir} ] || mkdir -p ${dir}
done
# other directory initialize
# mkdir ........
}
main
set_system_configuration.sh脚本负责设置os层面的个性化配置。
$ cat post-install/set_system_configuration.sh
#!/bin/bash
source $(dirname $0)/../config.ini
source $(dirname $0)/../common.sh
function set_system() {
echo "chmod 0644 /var/log/messages" >> /etc/rc.local
# other system configuration set
# ......
}
main() {
set_system
}
main
upgrade_kernel.sh负责设置kernel层面的个性化配置。
$ cat post-install/upgrade_kernel.sh
#!/bin/bash
source $(dirname $0)/../config.ini
source $(dirname $0)/../common.sh
kernel_upgrade() {
echo "kernel.pid_max = 262144" >> /etc/sysctl.conf
# other kernel set
# ......
}
main() {
kernel_upgrade
}
main
check.sh是用于被克隆机器reboot后,用户自定义的检查机器克隆正确性的脚本。脚本中重定义了 trap ‘command’ ERR的信号捕获处理逻辑。当ERR信号被捕获时,会调用checkret函数,将有问题的检查项内容输出给iclone。并且会将get_run_exit_status函数作为第一个被检查的项,在这个函数中会读取被克隆机器重启之前run.sh执行的退出状态。
最后,如果所有check item检查项都成功,会通过捕获信号EXIT而触发check_exit_status函数,向iclone输出ALL Check OK的信息。
$cat check.sh
#!/bin/bash
source $(dirname $0)/config.ini
source $(dirname $0)/common.sh
trap 'exit_status=$?;check_exit_status $exit_status' EXIT
trap 'status=$?;checkret $status "$check_item";ret=1' ERR
function get_run_exit_status(){
run_exit_status=`cat /tmp/clone_run_exit_status`
run_exit_status_num=$((run_exit_status))
return $run_exit_status_num
}
function checkret() {
echo "$2 check FAILED"
}
function check_exit_status(){
if [ $1 -eq 0 ]; then
echo "ALL Check OK"
fi
}
ret=0
chmod a+x $0
check_item='run.sh status'
get_run_exit_status
###################### there is the start of customed check items ########################
check_item='Redhat releas'
grep 7.2 /etc/redhat-release
# check_item=......
# command ......
# check_item=......
# command ......
###################### there is the end of customed check items ########################
exit $ret
信号捕获的屏蔽
以上脚本逻辑中,一旦信号捕获被设置后,将在脚本的全局生效。但是也有如下一些情况,需要屏蔽信号捕获。
需要获取标准输出信息时,例如 docker_info=(rpm−qa|grep−idocker),可以通过如下命令替换的方法屏蔽信号捕获。dockerinfo= ( r p m − q a | g r e p − i d o c k e r ) , 可 以 通 过 如 下 命 令 替 换 的 方 法 屏 蔽 信 号 捕 获 。 d o c k e r i n f o = (trap ‘exit 0’ ERR;rpm -qa | grep -i docker)。
不需要获取标准输出信息时,例如add_ali_public_net_route,可以通过如下子shell的方法屏蔽信号捕获。(trap ‘exit 0’ ERR;add_ali_public_net_route)。
模板重构效果
重构前代码
df -h | grep -Ew "/home/admin"
if [ $? -ne 0 ]; then
echo "required dir not mounted"
exit 1
fi
重构后代码
df -h | grep -Ew "/home/admin"
从以上对比可看到,代码将变得大为简洁和可靠。