bash脚本测试总结

bash脚本测试总结

基础参考:

https://zhuanlan.zhihu.com/p/102176365

https://zhuanlan.zhihu.com/p/99558152

https://zhuanlan.zhihu.com/p/101221388

 
跟踪脚本的执行
可以让bash打印出你脚本执行的过程中的所有语句。这很简单,只需要使用bash的-x选项就可以做到,下面让我们来看一下。
 
下面的这段脚本,先是输出一个问候语句,然后输出当前的时间:
#!/bin/bash
echo "Hello $USER,"
echo "Today is $(date +'%Y-%m-%d')"
 
下面让我们使用-x选项来运行这段脚本:
$ bash -x example_script.sh
+ echo 'Hello chenhao,'
Hello chenhao,
++ date +%Y-%m-%d
+ echo 'Today is 2009-08-31'
Today is 2009-08-31
 
这时,我们可以看到,bash在运行前打印出了每一行命令。而且每行前面的+号表明了嵌套。
这样的输出可以让你看到命令执行的顺序并可以让你知道整个脚本的行为。
 
在跟踪里输出行号
在一个很大的脚本中,你会看到很多很多的执行跟踪的输出,阅读起来非常费劲,所以,你可以在每一行前加上文件的行号,这会非常有用。要做到这样,你只需要设置下面的环境变量:
export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: '
让我们看看设置上了PS4这个环境变量后会是什么样的输出。
$ bash -x example_script.sh
+example_script.sh:2:: echo 'Hello chenhao,'
Hello chenhao,
++example_script.sh:3:: date +%Y-%m-%d
+example_script.sh:3:: echo 'Today is 2009-08-31'
Today is 2009-08-31
 
调试部份的脚本
有些时候,你并不想调试整个脚本,你只要调试其中的一部份。
那么,你可以在你想要调试的脚本之前,调用“set -x”,结束的时候调用“set +x”就可以了。
如下面的脚本所示:
#!/bin/bash
echo "Hello $USER,"
set -x
echo "Today is $(date %Y-%m-%d)"
set +x
 
让我们看看运行起来是啥样?
$ ./example_script.sh
Hello chenhao,
++example_script.sh:4:: date +%Y-%m-%d
+example_script.sh:4:: echo 'Today is 2009-08-31'
Today is 2009-08-31
+example_script.sh:5:: set +x
 
注意:我们在运行脚本的时候,不需要使用bash -x了。
 
日志输出
跟踪日志有时候太多了,多得都受不了,而且,输出的内容很难阅读。
一般来说,我们很多时候只关心于条件表达式,变量值,或是函数调用,或是循环等。。在这种情况下,log一些感兴趣的特定的信息,可能会更好。
使用log前,我们先写一个函数:
_log() {
  if [ "$_DEBUG" == "true" ]; then
    echo 1>&2 "$@"
  fi
}
 
于是,你就可以在你的脚本中如下使用:
_log "Copying files..."
cp src/* dst/
 
我们可以看到,上面那个_log函数,需要检查一个_DEBUG 变量,只有这个变量是真,才会真正开发输出日志。这样,你就只需要控制这个开关,而不需要删除你的debug信息。
$ _DEBUG=true ./example_script.sh
 
 
 

BASH 的调试手段

Wen Pingbo 创作于 2015/06/06

By WEN Pingbo of TinyLab.org 2015/06/01

平时在写 BASH 脚本时,总是会碰到让人抓狂的 BUG。和 C/C++ 这么丰富的调试工具相比,BASH 又有什么调试手段呢?

1 echo/print (普通技)

打印一些变量,或者提示信息。这应该是一个通用的方法了。在 BASH 里,我们可以简单的用 echo,或者 print 来输出一些 log,或者加一些 loglevel 来过滤一些 log。这里贴一下我平常用的函数:

_loglevel=2
 
DIE() {
echo "Critical: $1" >&2
exit 1
}
 
INFO() {
[ $_loglevel -ge 2 ] && echo "INFO: $1" >&2
}
 
ERROR() {
[ $_loglevel -ge 1 ] && echo "ERROR: $1" >&2
}

这里的实现只是简单的加了一个 loglevel,其实可以把 log 输出到一个文件中,或者给 log 加上颜色。比如:

# add color
[ $_loglevel -ge 1 ] && echo -e "\033[31m ERROR:\033[0m $1" >&2
# redirect to file
[ $_loglevel -ge 1 ] && echo "ERROR: $1" > /var/log/xxx_log.$BASHPID
 

2 set -x (稀有技)

-x(xtrace) 选项会导致 BASH 在执行命令之前,先把要执行的命令打印出来。这个选项对调试一些命令错误很有帮助。

有的时候,由于传进来的参数带有一些特殊字符,导致 BASH 解析时不是按照我们预想的进行。这个时候,把 -x 打开,就能在命令执行前,把扩展后的命令打印出来。比如基于前面写的函数:

set -x
INFO "this is a info log"
ERROR "this is a error log"
set +x

然后就可以看到如下输出:

+ INFO 'this is a info log'
+ '[' 2 -ge 2 ']'
+ echo -e '\033[32m INFO:\033[0m this is a info log'
INFO: this is a info log
+ ERROR 'this is a error log'
+ '[' 2 -ge 1 ']'
+ echo -e '\033[33m ERR:\033[0m this is a error log'
ERR: this is a error log
+ set +x

如果想全程打开 xtrace,可以在执行脚本的时候加 -x 参数。

3 trap/bashdb (史诗技)

为了方便调试,BASH 也提供了陷阱机制。这跟之前介绍的两种方法高级不少。我们可以利用 trap 这个内置命令来指定各个 sigspec 应该执行的命令。trap 的具体用法如下:

trap [-lp] [[arg] sigspec ...]

sigspec 包括 <signal.h> 中定义的各个 signal, EXIT,ERR,RETURN 和 DEBUG。

各个 signal 这里就不介绍了。EXIT 会在 shell 退出时执行指定的命令。若当前 shell 中有命令执行返回非零值,则会执行与 ERR 相关联的命令。而 RETURN 是针对 source 和 .,每次执行都会触发 RETURN 陷阱。若绑定一个命令到 DEBUG,则会在每一个命令执行之前,都会先执行 DEBUG 这个 trap。这里要注意的是,ERR 和 DEBUG 只在当前 shell 有效。若想函数和子 shell 自动继承这些 trap,则可以设置 -T(DEBUG/RETURN) 和 -E(ERR)。

比如,下面的脚本会在退出时,执行echo:

#!/bin/bash
 
trap "echo this is a exit echo" EXIT
 
echo "this is a normal echo"

或者,让脚本中命令出错时,把相应的命令打印出来:

#!/bin/bash
 
trap 'echo $BASH_COMMAND return err' ERR
 
echo this is a normal test
UnknownCmd

这个脚本的输出如下:

this is a normal test
tt.sh: line 6: UnknownCmd: command not found
UnknownCmd return err

亦或者,让脚本的命令单步执行:

#!/bin/bash
 
trap '(read -p "[$0 : $LINENO] $BASH_COMMAND ?")' DEBUG
 
echo this is a test
 
i=0
while [ true ]
do
echo $i
((i++))
done

其输出如下:

[tt.sh : 5] echo this is a test ?
this is a test
[tt.sh : 7] i=0 ?
[tt.sh : 8] [ true ] ?
[tt.sh : 10] echo $i ?
0
[tt.sh : 11] ((i++)) ?
[tt.sh : 8] [ true ] ?
[tt.sh : 10] echo $i ?
1
[tt.sh : 11] ((i++)) ?
[tt.sh : 8] [ true ] ?
[tt.sh : 10] echo $i ?
2
[tt.sh : 11] ((i++)) ?

是不是有点意思了?其实有一个 bashdb 的开源项目,也是利用 trap 机制,模拟 gdb 做了一个 bash 脚本的调试器。它本身也是一个 bash 脚本。在加载要调试的脚本后,可以用和 gdb 类似的命令,甚至缩写也是一样的,大家可以尝试一下:)

(上个月沉迷于 Diablo3,最后发现自己脸不行,悴!还是回来写点东西吧!)

Read More:

Shell脚本测试总结
 

1. 脚本测试的苦难
因为脚本使用的自由度很大,对于程序员限制很少,功能实现的随意性给测试带来了不少困难。首先,很多Shell脚本编写不规范,没有同意的Shell脚本编程规范,其次,脚本参数配置与程序逻辑混杂,区分不清晰。往往脚本作者同时承担多个开发任务,由于开发周期以及复杂的线上环境等原因,与其他脚本接口的沟通难以面面俱到,导致RD单元测试进行得很不充分。

2. 我们应该如何入手
首先,代码走查结合动态单步跟踪以及观察日志与文件输出,网络、CPU状态。
然后,撰写测试桩与驱动,白盒测试保证代码逻辑中循环和分支都能够走到,黑盒测试保证函数和功能脚本接口正确,输入输出符合设计预期。
对于异常处理,特别是变量的检查需要特别关注,变量在使用前都需要进行检查,是否为空?或者为0?对于文件名和路径必须检查,确认文件是否存在,路径是否可达之后再进行后续操作。
另外,需要考虑所依赖的其他功能脚本以及二进制工具,这些功能性单元应该如何使用,调用后的返回会有哪些情况,对于正常和异常结果,脚本是否能够捕捉到并且作出正确的判断。

3. 静态测试 && 动态测试
1) 新旧版本代码对比
可以基于icafe平台的codereview功能查看新旧版本的diff代码行,对比升级点,及时与RD沟通确认,避免遗漏,保证测试的全面性。代码对比的方式可能局限性比较大,适用于两个连续版本间代码结构无大的改动的情况,很多情况下,新版本的脚本会与之前的版本完全不同,Shell脚本与C语言模块有一个很大的区别就是,IM 模块C 代码的前后版本实现的承接关系很明显,但是Shell脚本不一定,可能后来的RD会将之前版本的脚本完全推翻。代码结构完全不同,因此在这种情况下,我们应该直接进入代码走查环节。

2) 代码走查
全面、深入、细致地关注脚本分支、循环逻辑正确性。
例如:retrbs重启脚本,在重启PS平台所有retrbs之后,需要清理PS平台retras cache,新增的启动方式升级分成两种启动方式,normal与continue模式,实际在codereview时发现normal方式重启完成后清理cache,continue方式重启完成后直接退出,这肯定是有问题的,因为按正常逻辑来说,不管那种启动方式,在重启完成之后都需要清理cache。


3) 搭建环境
搭建环境需要了解脚本的运行场景,运行频率,环境依赖以及与其配合的上下文脚本及程序:
脚本执行时所处的目录和配置文件
对应的产品模块功能
数据的周期性更新
server间的ssh认证
网络通信端口检查
脚本中的使用的工具
脚本硬件要求
比如说:脚本在什么目录下执行,每天几点钟执行,执行的时候需要什么数据以及工具提前准备好,等等。


4. 如何调试Shell脚本
1) 检查语法错误:
一般来说我们可以通过修改shell脚本的源代码,令其输出相关的调试信息来定位错误,那有没有不修改源代码来调试shell脚本的方法呢?答案就是使用shell的执行选,下面是一些常用选项的用法:
-n 只读取shell脚本,但不实际执行
-x 进入跟踪方式,显示所执行的每一条命令
-c "string" 从strings中读取命令

“-n”可用于测试shell脚本是否存在语法错误,但不会实际执行命令。在shell脚本编写完成之后,实际执行之前,首先使用“-n”选项来测试脚本是否存在语法错误是一个很好的习惯。因为某些shell脚本在执行时会对系统环境产生影响,比如生成或移动文件等,如果在实际执行才发现语法错误,您不得不手工做一些系统环境的恢复工作才能继续测试这个脚本。

“-c”选项使shell解释器从一个字符串中而不是从一个文件中读取并执行shell命令。当需要临时测试一小段脚本的执行结果时,可以使用这个选项,如下所示:
sh -c 'a=1;b=2;let c=$a+$b;echo "c=$c"'

"-x"选项可用来跟踪脚本的执行,是调试shell脚本的强有力工具。“-x”选项使shell在执行脚本的过程中把它实际执行的每一个命令行显示出来,并且在行首显示一个"+"号。 "+"号后面显示的是经过了变量替换之后的命令行的内容,有助于分析实际执行的是什么命令。 “-x”选项使用起来简单方便,可以轻松对付大多数的shell调试任务,应把其当作首选的调试手段。

2) 调试工具-bashdb
使用shell调试器bashdb,这是一个类似于GDB的调试工具,可以完成对shell脚本的断点设置,单步执行,变量观察等许多功能。

使用bashdb进行debug的常用命令
1.列出代码和查询代码类:
l 列出当前行以下的10行
- 列出正在执行的代码行的前面10行
. 回到正在执行的代码行
w 列出正在执行的代码行前后的代码
/pat/ 向后搜索pat
?pat?向前搜索pat

2.Debug控制类:
h 帮助
help 命令 得到命令的具体信息
q 退出bashdb
x 算数表达式 计算算数表达式的值,并显示出来
!!空格Shell命令 参数 执行shell命令
使用bashdb进行debug的常用命令(cont.)
控制脚本执行类:
n 执行下一条语句,遇到函数,不进入函数里面执行,将函数当作黑盒
s n 单步执行n次,遇到函数进入函数里面
b 行号n 在行号n处设置断点
del 行号n 撤销行号n处的断点
c 行号n 一直执行到行号n处
R 重新启动
Finish 执行到程序最后
cond n expr 条件断点


5. 脚本测试的基本流程
1.静态代码检查
2.单元测试1:针对每个功能函数撰写驱动和桩,验证所有分支
• 确认每个配置项以及设计的文件目录是否在使用前进行检查
• 确认所有的变量没有向外传播的危险
• 确认所产出的临时文件没有泄露,脚本自己会负责处理掉临时文件
3.单元测试2:对于单个功能脚本sh -x XXX.sh 跟踪脚本执行情况
4.集成测试1:对于所有脚本使用sh -x XXX.sh 跟踪脚本执行情况
5.集成测试2:模拟脚本生产环境,周期性连续多次执行全部功能脚本,监控脚本性能以及日志、临时文件等状态。

6. 脚本测试中遇到的问题和解决方案
1) 判断一个数组是否为空:
【脚本内容】: 
if [ -z ${pg_readyDatalist[@]} ]
then
…………
fi 
【问题】:不可如此判断,超过一个元素时,语法错误
【sh -x 执行】: 
+ '[' -z model gtrindex ']'
retrbs_restart.sh: line 366: [: model: binary operator expected
【原因】: 
-z 只能判断一个变量是否为空
判断一个list是否为空,应该:
【解决】判断list元素个数是否为0
例如: if [ ${#ps_retrbs[@]} -eq 0 ]

2) If语句判断
【脚本内容】: 
if [ -f ./$i] 
then 
echo "test" 
fi 
【问题】: .$i] 的“]”前面没有空格,造成语法错误
【sh -x 执行】:./test.sh: line 3: [: missing `] 
【原因】: If语句的条件判断“[ ]”,“[”之后和“]”之前必须有空格
【解决】加上空格


3) 字符串判断
【脚本内容】: 
if [ "$1" = "continue" ] then 
echo “succ”
fi 
【问题】:$1为空,打印“succ”
【sh -x 执行】:succ 
【原因】: $1为空会造成语法错误,返回0,继续执行if代码块中的逻辑,导致判断错误 
【解决】修改成 if [ "a$1" = "acontinue" ]

4) 变量传播
【脚本内容】: 
func(){
for((i=0;i<$RETRY_TIMES;i++))
do
NOTICE "delBlacklist”
done
}
for (( i=0; i<pggroup_size; i++))
do
func()
done
【问题】:“i”的值自增之后会传递到外层调用脚本,导致外层调用脚本的循环跳过或死循环
【解决】避免使用i,j,k等常见的循环控制变量,使用自定义的变量名,如retry_count等
在shell函数中定义的变量加上local关键字


5) 命令连接
问题一:
【脚本内容】: 
cd to_del; rm -rf *
【问题】:如果cd 目录失败,rm -rf * 会错误地删除当前目录下的所有文件
【解决】使用 && 连接 cd失败将不会继续执行后面的命令

问题二:
【脚本内容】: 
for data in ${datalist{@}}
do
runRemoteCmd ${host} "cd ${data_path}.new && [[ -f ${data_flag} ]]" || suc=0 && break
done 
【问题】:这里的 || && 是同一个优先级
那么就是说 && 后面的语句 break无论什么情况下都不可能被执行到
【解决】拆成两条语句,单独判断suc


6) 文件泄露
【脚本内容】: 
local status=$( mySsh ${remote_host} "{ ${command%%;}; }&>/tmp/$$ && echo 0 || echo 1" ) 
【问题】:上述代码将远程执行命令行的输出结果导入到一个以pid命名的临时文件中,在脚本关闭的时候没有清除,每一次执行将创建一个新文件,很可能导致文件泄露问题。
【解决】注意清理脚本生成的临时文件

 

7) ssh 远程执行后台命令不靠谱
【脚本内容】: 
ssh hostname "cat bin &“
【执行】
[work@www.baidu.com bin]$ ssh localhost "cat bin &"
cat: bin: Is a directory
[work@www.baidu.com bin]$ echo $?

【问题】:命令执行错误,返回值为0 
【解决】将远程命令放在前台执行:
[work@www.baidu.com bin]$ ssh localhost "cat bin" 
cat: bin: Is a directory
[work@www.baidu.com bin]$ echo $?
1

 

8) 变量使用前使用unset清理
【脚本内容】:一般是针对脚本的配置文件 
ps_retras[0]="work@www.baidu.com"
ps_retras[1]="work@www.baidu.com"
【问题】:如果OP修改ps_retras数组的配置,可能无法生效
【解决】使用unset进行清理 
unset
功能说明:删除变量或函数。 
语法:unset [-fv][变量或函数名称] 
参数: 
-f 仅删除函数。 
-v 仅删除变量。 
例如:unset ps_retras
ps_retras[0]="work@www.baidu.com"
ps_retras[1]=“work@www.baidu.com“

 

7. shell 内置变量
1) $FUNCNAME
函数的名字,类似于C语言中的内置宏__func__,但宏__func__ 只能代表当前所在的函数名,而$FUNCNAME的功能更强大,它是一个数组变量,其中包含了整个调用链上所有的函数的名字,故变量${FUNCNAME [0]}代表shell脚本当前正在执行的函数的名字,而变量${FUNCNAME[1]}则代表调用函数${FUNCNAME[0]}的函数的名字,依此类推。

2) $BASH_SOURCE
shell脚本源文件名,与FUNCNAME相对应

3) $BASH_LINENO
代表shell脚本的当前行号,类似于C语言中的内置宏__LINE__,与FUNCNAME相关联
BASH_LINENO[$i] 指示的是 FUNCNAME[$i + 1]被调用的位置

4) $PS4
第四级提示符变量$PS4 , $PS4的值将被显示在“-x”选项输出的每一条命令的前面。在Bash Shell中,缺省的$PS4的值是"+"号。(现在知道为什么使用"-x"选项时,输出的命令前面有一个"+"号了吧 )
通过修改$PS4的值,就可以达到sh –x 时显示行号还有函数名称的目的了。

 
================= End
 
 

 

posted @ 2019-04-30 17:03  lsgxeva  阅读(3471)  评论(0编辑  收藏  举报