转载自:Shell调试技巧
首先, 调试要比编写代码困难得多, 因此, 如果你尽可能聪明的编写代码, 你就不会在调试的时候花费很多精力. | Brian Kernighan |
---|
Bash并不包含调试器, 甚至都没有包含任何用于调试目的的命令和结构. 脚本中的语法错误, 或者拼写错误只会产生模糊的错误信息, 当你调试一些非功能性脚本的时候, 这些错误信息通常都不会提供有意义的帮助.
例子 29-1. 一个错误脚本
#!/bin/bash
# ex74.sh
# 这是一个错误脚本.
# 哪里出了错?
a=37
if [$a -gt 27 ]
then
echo $a
fi
exit 0
脚本的输出:
./ex74.sh: [37: command not found
上边的脚本究竟哪错了(提示: 注意if的后边)?
例子 29-2. 缺少关键字
#!/bin/bash
# missing-keyword.sh: 这个脚本会产生什么错误?
for a in 1 2 3
do
echo "$a"
# done # 第7行上的关键字done'被注释掉了.
exit 0
脚本的输出:
missing-keyword.sh: line 10: syntax error: unexpected end of file
注意, 其实不必参考错误信息中指出的错误行号. 这行只不过是Bash解释器最终认定错误的地方.
出错信息在报告产生语法错误的行号时, 可能会忽略脚本的注释行.
如果脚本可以执行, 但并不如你所期望的那样工作, 怎么办? 通常情况下, 这都是由常见的逻辑错误所产生的.
例子 29-3. test24, 另一个错误脚本
#!/bin/bash
# 这个脚本的目的是删除当前目录下的某些文件,
#+ 这些文件特指那些文件名包含空格的文件.
# 但是不能如我们所愿的那样工作.
# 为什么?
badname=`ls | grep ' '`
# 试试这个:
# echo "$badname"
rm "$badname"
exit 0
为了找出例子 29-3中的错误, 我们可以把echo "$badname"行的注释符去掉. echo出来的信息能够帮助你判断脚本是否按你期望的方式运行.
在这个特定的例子里, rm "$badname"之所以没有给出期望的结果, 是因为$badname
不应该被引用起来. 加上引号会保证rm只有一个参数(这就只能匹配一个文件名). 一种不完善的解决办法是去掉$badname
外面的引号, 并且重新设置$IFS
, 让$IFS
只包含一个换行符, IFS=$'\n'. 但是, 下面这个方法更简单.
# 删除文件名中包含空格的文件, 下面这才是正确的方法.
rm *\ *
rm *" "*
rm *' '*
# 感谢. S.C.
总结一下这个脚本的症状,
- 由于"syntax error"(语法错误)使得脚本停止运行,
- 或者脚本能够运行, 但是并不是按照我们所期望的那样运行(逻辑错误).
- 脚本能够按照我们所期望的那样运行, 但是有烦人的副作用(逻辑炸弹).
如果想调试不工作的脚本, 有如下工具可用:
1. echo语句可以放在脚本中存在疑问的位置上, 来观察变量的值, 也可以了解脚本后续的动作.
### debecho (debug-echo), 由Stefano Falsetto编写 ###
### 只有在DEBUG变量被赋值的情况下, 才会打印传递进来的参数. ###
debecho () {
if [ ! -z "$DEBUG" ]; then
echo "$1" >&2
# ^^^ 打印到stderr
fi
}
DEBUG=on
Whatever=whatnot
debecho $Whatever # whatnot
DEBUG=
Whatever=notwhat
debecho $Whatever # (这里就不会打印.)
2. 使用过滤器tee来检查临界点上的进程或数据流.
3. 设置选项-n -v -x
sh -n scriptname
不会运行脚本, 只会检查脚本的语法错误. 这等价于把set -n
或set -o noexec
插入脚本中. 注意, 某些类型的语法错误不会被这种方式检查出来.
sh -v scriptname
将会在运行脚本之前, 打印出每一个命令. 这等价于把set -v
或set -o verbose
插入到脚本中.
选项-n
和-v
可以同时使用. sh -nv scriptname
将会给出详细的语法检查.
sh -x scriptname
会打印出每个命令执行的结果, 但只使用缩写形式. 这等价于在脚本中插入set -x
或set -o xtrace
.
把set -u
或set -o nounset
插入到脚本中, 并运行它, 就会在每个试图使用未声明变量的地方给出一个unbound variable错误信息.
4. 使用"assert"(断言)函数在脚本的临界点上测试变量或条件. (这是从C语言中引入的.)
例子 29-4. 使用"assert"来测试条件
#!/bin/bash
# assert.sh
assert () # 如果条件为false,
{ #+ 那么就打印错误信息并退出脚本.
E_PARAM_ERR=98
E_ASSERT_FAILED=99
if [ -z "$2" ] # 传递进来的参数个数不够.
then
return $E_PARAM_ERR # 什么都不做就return.
fi
lineno=$2
if [ ! $1 ]
then
echo "Assertion failed: \"$1\""
echo "File \"$0\", line $lineno"
exit $E_ASSERT_FAILED
# else
# 返回
# 然后继续执行脚本余下的代码.
fi
}
a=5
b=4
condition="$a -lt $b" # 产生错误信息并退出脚本.
# 尝试把这个"条件"放到其他的地方,
#+ 然后看看发生了什么.
assert "$condition" $LINENO
# 只有在"assert"成功时, 脚本余下的代码才会继续执行.
# 这里放置的是其他的一些命令.
# ...
echo "This statement echoes only if the \"assert\" does not fail."
# ...
# 这里也放置其他一些命令.
exit 0
5. 使用变量$LINENO和内建命令caller.
6. 捕获exit.
脚本中的exit命令会触发一个信号0, 这个信号终止进程, 也就是终止脚本本身. 捕获exit在某些情况下很有用, 比如说强制"打印"变量值. trap命令必须放在脚本中第一个命令的位置上.
捕获信号
-
trap
-
可以在收到一个信号的时候指定一个处理动作; 在调试的时候, 这一点也非常有用.
A signal就是发往进程的一个简单消息, 这个消息即可以由内核发出, 也可以由另一个进程发出, 发送这个消息的目的是为了通知目的进程采取一些指定动作(通常都是终止动作). 比如说, 按下Control-C, 就会发送一个用户中断(即INT信号)到运行中的进程.
trap '' 2
# 忽略中断2(Control-C), 没有指定处理动作.
trap 'echo "Control-C disabled."' 2
# 当Control-C按下时, 显示一行信息.
例子 29-5. 捕获exit
#!/bin/bash
# 使用trap来捕捉变量值.
trap 'echo Variable Listing --- a = $a b = $b' EXIT
# EXIT是脚本中exit命令所产生信号的名字.
#
# "trap"所指定的命令并不会马上执行,
#+ 只有接收到合适的信号, 这些命令才会执行.
echo "This prints before the \"trap\" --"
echo "even though the script sees the \"trap\" first."
echo
a=39
b=36
exit 0
# 注意, 即使注释掉上面的这行'exit'命令, 也不会产生什么不同的结果,
#+ 这是因为所有命令都执行完毕后, 不管怎么样, 脚本都会退出的.
例子 29-6. Control-C之后, 清除垃圾
#!/bin/bash
# logon.sh: 一个检查你是否在线的脚本, 这个脚本实现的很简陋.
umask 177 # 确保temp文件并不是所有用户都有权限访问.
TRUE=1
LOGFILE=/var/log/messages
# 注意: $LOGFILE必须是可读的
#+ (使用root身份来执行, chmod 644 /var/log/messages).
TEMPFILE=temp.$$
# 使用脚本的进程ID, 来创建一个"唯一"的临时文件名.
# 也可以使用'mktemp'.
# 比如:
# TEMPFILE=`mktemp temp.XXXXXX`
KEYWORD=address
# 登陆时, 把"remote IP address xxx.xxx.xxx.xxx"
# 添加到/var/log/messages中.
ONLINE=22
USER_INTERRUPT=13
CHECK_LINES=100
# 日志文件有多少行需要检查.
trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT
# 如果脚本被control-c中途中断的话, 那么就清除掉临时文件.
echo
while [ $TRUE ] #死循环.
do
tail -$CHECK_LINES $LOGFILE> $TEMPFILE
# 将系统日志文件的最后100行保存到临时文件中.
# 这么做很有必要, 因为新版本的内核在登陆的时候, 会产生许多登陆信息.
search=`grep $KEYWORD $TEMPFILE`
# 检查是否存在"IP address"片断,
#+ 它用来提示, 这是一次成功的网络登陆.
if [ ! -z "$search" ] # 必须使用引号, 因为变量可能会包含一些空白符.
then
echo "On-line"
rm -f $TEMPFILE # 清除临时文件.
exit $ONLINE
else
echo -n "." # echo的-n选项不会产生换行符.
#+ 这样你就可以在一行上连续打印.
fi
sleep 1
done
# 注意: 如果你将变量KEYWORD的值改为"Exit",
#+ 当在线时, 这个脚本就可以被用来检查
#+ 意外的掉线情况.
# 练习: 按照上面"注意"中所说的那样来修改这个脚本,
# 让它表现的更好.
exit 0
# Nick Drage建议使用另一种方法:
while true
do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0
echo -n "." # 不停的打印(.....), 用来提示用户等待, 直到连接上位置.
sleep 2
done
# 问题: 使用Control-C来终止这个进程可能是不够的.
#+ (可能还会继续打印"...")
# 练习: 修复这个问题.
# Stephane Chazelas提出了另一种方法:
CHECK_INTERVAL=1
while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD"
do echo -n .
sleep $CHECK_INTERVAL
done
echo "On-line"
# 练习: 讨论一下这几种不同方法
# 各自的优缺点.
如果使用trap命令的
DEBUG
参数, 那么当脚本中每个命令执行完毕后, 都会执行指定的动作. 比方说, 你可以跟踪某个变量的值.
例子 29-7. 跟踪一个变量
#!/bin/bash
trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
# 当每个命令执行之后, 就会打印出$variable的值.
variable=29
echo "Just initialized \"\$variable\" to $variable."
let "variable *= 3"
echo "Just multiplied \"\$variable\" by 3."
exit $?
# "trap 'command1 . . . command2 . . .' DEBUG"结构更适合于
#+ 使用在复杂脚本的上下文中,
#+ 如果在这种情况下大量使用"echo $variable"语句的话,
#+ 将会非常笨拙, 而且很耗时.
# 感谢, Stephane Chazelas指出这点.
# 脚本的输出:
VARIABLE-TRACE> $variable = ""
VARIABLE-TRACE> $variable = "29"
Just initialized "$variable" to 29.
VARIABLE-TRACE> $variable = "29"
VARIABLE-TRACE> $variable = "87"
Just multiplied "$variable" by 3.
VARIABLE-TRACE> $variable = "87"
当然, 除了调试之外, trap命令还有其他的用途.
例子 29-8. 运行多进程(在对称多处理器(SMP box)的机器上)
#!/bin/bash
# parent.sh
# 在多处理器(SMP box)的机器里运行多进程.
# 作者: Tedman Eng
# 我们下面要介绍两个脚本, 这是第一个,
#+ 这两个脚本都要放到当前工作目录下.
LIMIT=$1 # 想要启动的进程总数
NUMPROC=4 # 并发的线程数量(forks?)
PROCID=1 # 启动的进程ID
echo "My PID is $$"
function start_thread() {
if [ $PROCID -le $LIMIT ] ; then
./child.sh $PROCID&
let "PROCID++"
else
echo "Limit reached."
wait
exit
fi
}
while [ "$NUMPROC" -gt 0 ]; do
start_thread;
let "NUMPROC--"
done
while true
do
trap "start_thread" SIGRTMIN
done
exit 0
# ======== 下面是第二个脚本 ========
#!/bin/bash
# child.sh
# 在SMP box上运行多进程.
# 这个脚本会被parent.sh调用.
# 作者: Tedman Eng
temp=$RANDOM
index=$1
shift
let "temp %= 5"
let "temp += 4"
echo "Starting $index Time:$temp" "$@"
sleep ${temp}
echo "Ending $index"
kill -s SIGRTMIN $PPID
exit 0
# ======================= 脚本作者注 ======================= #
# 这个脚本并不是一点bug都没有.
# 我使用limit = 500来运行这个脚本, 但是在过了开头的一两百个循环后,
#+ 有些并发线程消失了!
# 还不能确定这是否是由捕捉信号的冲突引起的, 或者是其他什么原因.
# 一旦接收到捕捉的信号, 那么在下一次捕捉到来之前,
#+ 会有一段短暂的时间来执行信号处理程序,
#+ 在信号处理程序处理的过程中, 很有可能会丢失一个想要捕捉的信号, 因此失去一个产生子进程的机会.
# 毫无疑问的, 肯定有人能够找出产生这个bug的原因,
#+ 并且在将来的某个时候. . . 修正它.
# ===================================================================== #
#################################################################
# 下面的脚本是由Vernia Damiano原创.
# 不幸的是, 它并不能正常工作.
#################################################################
#!/bin/bash
# 要想调用这个脚本, 至少需要一个整形参数
#+ (并发的进程数).
# 所有的其他参数都传递给要启动的进程.
INDICE=8 # 想要启动的进程数目
TEMPO=5 # 每个进程最大的睡眠时间
E_BADARGS=65 # 如果没有参数传到脚本中, 那么就返回这个错误码.
if [ $# -eq 0 ] # 检查一下, 至少要传递一个参数给脚本.
then
echo "Usage: `basename $0` number_of_processes [passed params]"
exit $E_BADARGS
fi
NUMPROC=$1 # 并发进程数
shift
PARAMETRI=( "$@" ) # 每个进程的参数
function avvia() {
local temp
local index
temp=$RANDOM
index=$1
shift
let "temp %= $TEMPO"
let "temp += 1"
echo "Starting $index Time:$temp" "$@"
sleep ${temp}
echo "Ending $index"
kill -s SIGRTMIN $$
}
function parti() {
if [ $INDICE -gt 0 ] ; then
avvia $INDICE "${PARAMETRI[@]}" &
let "INDICE--"
else
trap : SIGRTMIN
fi
}
trap parti SIGRTMIN
while [ "$NUMPROC" -gt 0 ]; do
parti;
let "NUMPROC--"
done
wait
trap - SIGRTMIN
exit $?
: <<SCRIPT_AUTHOR_COMMENTS
我需要使用指定的选项来运行一个程序,
并且能够接受不同的文件, 而且要运行在一个多处理器(SMP)的机器上.
所以我想(我也会)运行指定数目个进程,
并且每个进程终止之后, 都要启动一个新进程.
"wait"命令并没有提供什么帮助, 因为它需要等待一个指定的后台进程,
或者等待*全部*的后台进程. 所以我编写了[这个]bash脚本程序来完成这个工作,
并且使用了"trap"指令.
--Vernia Damiano
SCRIPT_AUTHOR_COMMENTS
trap '' SIGNAL(两个引号之间为空)在剩余的脚本中禁用了SIGNAL信号的动作. trap SIGNAL则会恢复处理SIGNAL的动作. 当你想保护脚本的临界部分不受意外的中断骚扰, 那么上面讲的这种办法就非常有用了.
trap '' 2 # 信号2就是Control-C, 现在被禁用了.
command
command
command
trap 2 # 重新恢复Control-C
Bash3.0之后增加了如下这些特殊变量用于调试.
$BASH_ARGC
$BASH_ARGV
$BASH_COMMAND
$BASH_EXECUTION_STRING
$BASH_LINENO
$BASH_SOURCE
$BASH_SUBSHELL
注意事项
[1] | 事实上, Rocky Bernstein的Bash debugger填补了这项空白. |
---|---|
[2] | 根据惯例, *信号0* 被指定为exit. |
摘自:高级Bash脚本编程指南: 一本深入学习shell脚本艺术的书籍
参考:
标签:脚本,#+,Shell,技巧,echo,exit,trap,variable,调试 From: https://www.cnblogs.com/jyou/p/16927346.html