简介
在我们写代码过程中,一般有两个阶段:调试阶段和试运行阶段。在调试阶段我们希望尽可能的输出日志,方便在出错的时候快速定位问题。在试运行阶段希望将日志标准化,且有些错误的日志是在预期内不想展示的时候如何处理,这篇基础文章将介绍这两个阶段如果有效的节约编程时间。
目录
1. 脚本调试
1.1. 日志输出
1.2. debug调试
2. 运行shell脚本的异常报错
2.1. 找不到命令
2.2. 语法缺少结束符
2.3. 部分命令无法执行(巨坑)
3. 错误处理
3.1. 异常状态码
3.2. 正常、异常日志重定向
1. 脚本调试
我们在编写脚本时,调试时需要用到2种方法:
- 每个任务点输出有效日志;
- 出错时怎样查看详细信息。
1.1. 日志输出
如何通过输出日志达到调试的目的呢?
我们可以使用 echo 或者 printf 命令来输出当前的任务情况。例如:其中一个任务为监控磁盘大小
path="/home/yt"
while true;do
size=$(df -h ${path} |awk 'NR==2{print $4}')
echo "`date '+%Y-%m-%d %H:%M:%S'` [INFO] The available disk space is ${size}"
sleep 10
done
在代码中,我们使用了时间+类型+信息的方式汇报结果,这可以使得我们对某个任务的执行时间和做的事情有很清晰的了解。
如果觉得每次输出日志都需要加一下时间之类的东西很麻烦,不妨试试用函数封装一个方法
PrintLog(){
local str_type="$1"
local str="$2"
local result="$3"
local current_time="$(date '+%Y-%m-%d %H:%M:%S')"
printf "${current_time} [${str_type}] %-50s ${result}\n" "${str}"
}
- str_type:日志类型(自定义:INFO、WARNING、ERROR、DEBUG等)。
- str:自定义日志信息。
- result:最终结果(自定义:SUCCEED、FAILED等)。
准备好一个简易版的日志输出方法,来检验一下
# 函数名 "类型" "输出的字符" "最终结果"
PrintLog "INFO" "Check the running IP address" "SUCCEED"
PrintLog "INFO" "Check the system configuration" "FAILED"
按照预期输出了时间、类型、字符串、结果。
但这还不够,我们再来改进一下:
- result 每次输入 SUCCEED 或 FAILED 太麻烦了,直接用 1 和 0 替代。
- result 需要支持 不输出结果、自定义结果、0或1选项。
- 如果 result 为 FAILED,则退出程序。
PrintLog(){
local str_type="$1"
local str="$2"
local result=$3
local current_time="$(date '+%Y-%m-%d %H:%M:%S')"
# 如果第3个字符为0,表示失败
if [ ${result} -eq 0 ];then
result="FAILED"
# 如果第3个字符为1,表示成功
elif [ ${result} -eq 1 ];then
result="SUCCEED"
fi
# 输出日志信息
printf "${current_time} [${str_type}] %-50s ${result}\n" "${str}"
# 如果result="FAILED",则退出程序
[ ${result} == "FAILED" ] && exit 1
}
优化代码后再假装执行3个任务
echo "============= 执行任务1 ============="
PrintLog "INFO" "Perform Task 1" 1
echo "============= 执行任务2 ============="
PrintLog "INFO" "Perform Task 2" 0
echo "============= 执行任务3 ============="
PrintLog "INFO" "Perform Task 3" 1
结果如下:
在执行任务2时,指定 result 为0(表示异常),所以shell在执行完第2个任务后自动终止脚本。
这种方法怎么去应用呢?
# 执行一个ls命令
ls abcd
# 如果这个命令执行失败,那么输入指定日志后退出脚本
[ $? -ne 0 ] && PrintLog "ERROR" "Run the ls command" 0
echo "============= 执行任务1 ============="
PrintLog "INFO" "Perform Task 1" 1
通过 $? 判断上一个命令是否正常,如果不正常则输出错误信息并退出
一般情况下,脚本中都含有多个任务,这些任务一般都由函数封装。对使用者来说:每个任务输出一行信息就行,对我们编写者来说:能少写一行就少写一行。所以,在日志输入上,主任务中输出一行有效日志即可。当发现某个主任务出现了异常但没找到问题时,我们可以继续在出现问题这个函数中输出更详细的日志。
1.2. debug调试
在 shell 一般使用 bash -x 来调试脚本。一般情况下,我们基本可以通过系统本身抛出的错误来迅速找到代码的问题,但有一些问题是无法通过系统提示定位问题的。比如:进程卡住
# 监控磁盘大小
MonitorDisk(){
path="/home/yt"
while true;do
local size=$(df -h ${path} |awk 'NR==2{print $4}')
echo "`date '+%Y-%m-%d %H:%M:%S'` [INFO] The available disk space is ${size}"
sleep 10
done
}
# 监控内存大小
MonitorMemory(){
while true;do
# 将监控磁盘作为子进程,同时监控两种状态
MonitorDisk &
local mem_free=$(free -h |awk 'NR==2{print $4}')
echo "`date '+%Y-%m-%d %H:%M:%S'` [INFO] The remaining memory is ${mem_free}"
sleep 10
wait
done
}
MonitorMemory
这里写了一个错误的示例,将监控磁盘放到了监控内存里面,并且使用 wait 等待,结果如下:刚开始两种同时监控,但后面只监控到了磁盘
当出现这种不符合预期的情况,系统也没有报错,那么我们需要查看 debug 日志:
在这张图片中,分别出现了2种不同类型的日志:带+号、不带+号。
- 带+符号:表示脚本中的代码
- 不带+号:表示输出的日志信息
在这些带+号的代码中,又分别会出现1个、2个、3个、n个,这些实际上是级别表示:
- +:一级执行级别(顶层执行的命令,通常是整个脚本中的命令)。
- ++:二级执行级别(通常用于嵌套在一级执行级别命令中的命令)。
- +++:三级执行级别(更深度嵌套的代码或执行流程)。
我们来看一下这个脚本的信息
先看红框,这是两个任务的执行信息, 标注的1、2、3、4分别是它们的执行流程。
1:执行的内存监控
2:执行的磁盘监控
3:执行的磁盘监控
4:执行的磁盘监控
我们发现执行内存监控后就一直执行磁盘监控,而后内存监控没再工作。所以我们往1~3的中间找找其他日志,发现在2后面出现一个 wait 命令,使用 wait 后会持续等待子进程结束,所以,这个脚本的问题就在于 wait ,我们重新将函数和 wait 放入最后一行。
MonitorDisk & MonitorMemory & wait
最终结果:符合预期
2. 运行shell脚本的异常报错
shell 的运行有2个点需要注意:
- 如果脚本中出现语法不正确时并不会在执行前检查,而是在执行过程中发现语法错误后自动退出;
- 如果脚本中语法正确,但执行过程中的"命令"出错不会退出。
针对这两点我们来看看语法问题应该如何处理,有哪些坑需要注意。
- Linux中可以通过命令 shellcheck [脚本] 来检查脚本语法,这里就不对这个命令进行说明了。
2.1. 找不到命令
- 出现找不到命令的错误不会终止脚本,会继续执行。
echo "=========开始运行脚本========="
a # 执行一个错误的命令
echo "=========结束运行脚本========="
我们设置了 3 条命令,开始和结尾的命令是正常的,中间命令是不存在的,看一下结果:
运行结果如下:
- 【正常】执行第1条命令
- 【异常】执行第2条命令
- 【正常】执行第3条命令
中间出现了异常的命令,shell不会终止脚本,输出对应信息后继续往下执行。输出的信息:
- 【脚本路径】【异常行数】【异常命令】【异常提示】
正常的处理流程就是:
- 查看异常提示是什么
- 查看异常行数,vim +[行号] [脚本] 检查问题
- 最后修改问题
2.2. 语法缺少结束符
- 出现语法错误后会终止脚本,但不会提前检查。
echo "=========开始运行脚本========="
if [ 1 -eq 1 ];then
echo "正确"
echo "=========结束运行脚本========="
这里可以看到脚本的第1行正常执行,第2行的 if 判断因为语法问题而报错,报错以后直接退出,后面的代码不再执行。箭头处系统给出的报错文件是第9行,而我们文件总共才8行,哪来的第9行。我去查了一下资料没查到,有懂的小伙伴请评论区留言。所以我去总结了一下哪些情况会这样:
- if 判断缺少结束符 fi
- for 循环缺少结束符 done
- while 循环缺少结束符 done
- case 缺少结束符不会这样,会在缺少的那一行报错。
总的来说,只要看到报错的行数大于文件总行数,并且只输出了语法错误 没有具体的错误信息,那基本就是结尾符的问题了。
理解了这个异常是什么导致的以后,下一个问题来了,当我们脚本很大 又没有用函数封装,利用单行注释去调试又太慢,怎么办?
举个例子,这里有很多个 if
执行结果是这样的:前面正常执行,后面语法错误,抛出异常400行
我们用最简单的方法:过滤查找(缩小范围)
grep -nE "if|fi" [文件名]
使用 grep 输出包含 if 和 fi 的行号和信息,手动去检查,如果 if 下面缺少 fi 基本就能确定是哪行
13 和 17 行这里出现了2个 if ,一般嵌套很少有 if 嵌套 if,即使有也很少。通过这里我们发现 13 的 if 没有结束符,如果代码类似我这种情况2s搞定,如果比较复杂按缩进排查就行。
2.3. 部分命令无法执行(巨坑)
为什么说这个时巨坑,因为执行的时候不报错,bash -x 发现不了问题。这是之前在写一个shell过程中,拷贝代码过来导致中间一部分命令无法执行,找了半个小时才发现罪魁祸首是 EOF。
通过 <<EOF 可以在脚本中创建一个文本块,并将其传递给命令或程序。我的目的是使用root用户清理缓存
#将EOF中的文本传递给 su root 命令
su root <<-EOF
密码
sync
echo 3 > /proc/sys/vm/drop_caches
EOF
这样看起来没毛病吧,但我是函数,所以多加了一个 tab,再来看看效果
func(){
su root <<-EOF
密码
sync
echo 3 > /proc/sys/vm/drop_caches
EOF
}
func
在 EOF 前方加上 - 符号可以忽略 tab,所以这种写法是没问题的。问题出在我是拷贝过来的,拷贝过来的 tab 就变成了空格,这种情况加 - 符号也无效,所以结尾的 EOF 那里也就无效了。本来是应该抛出这个错误:
但由于脚本中拷贝了多个EOF,导致它没有抛出异常,而是直接忽略了中间那部分代码。这种情况使用 bash -x 直接不显示中间那部分函数,压根儿 不执行。
所以啊,在写EOF时一定不要用空格,不要拷贝!!!
3. 错误处理
3.1. 异常状态码
Linux 每执行一个任务或命令时都会返回一个状态码(范围 0~255),使用 $? 获取
0 :表示执行成功。
1-125:命令或脚本执行的常规错误代码。
126 :命令找到但无法执行。
127 :命令未找到。
128+ :通常表示命令或脚本因接收到异常信号而终止。
所以我们在判断一个命令是否执行成功,只需要使用 $?。例如执行一个异常的命令
返回的状态码非 0
再来执行一个正常的命令
返回状态码为 0
所以当我们判断一个命令是否执行成功时,可以这样写
ls "abc"
if [ $? -eq 0 ];then
echo "状态码为0,上一条命令执行成功!"
else
echo "状态码非0,上一条命令执行失败!"
fi
3.2. 正常、异常日志重定向
在 shell 中,系统抛出的日志分为正常和异常两种。当我们对某个命令所返回的结果重定向到另一个文件中时,系统会自动判断:如果命令执行成功则可以重定向到某个文件,如果命令执行失败则无法重定向到某个文件,直接输出到屏幕。
可以看到,执行成功的命令结果是可以重定向到文件 tmp.txt 中,而执行失败的结果是无法重定向到 tmp.txt 中。
如果我们必须将任务的执行结果输出到一个文件时(不论正常还是异常),那么可以通过 1 和 2 来指定
0
:表示标准输入stdin
,通常对应于键盘输入。1
:表示标准输出stdout
,通常对应于命令或脚本的正常输出。2
:表示标准错误输出stderr
,通常用于输出命令或脚本的错误信息。
使用 2>&1 将标准错误输出
stderr
重定向到标准输出stdout
上,所以可以输出到文件。
如果我们希望将错误日志和正常日志分开存放应该怎么处理呢?
使用 1 和 2 分开存放,将正常的日志追加到 info.log,将异常的日志追加到 err.log
如果不想输出日志又怎么处理呢?
直接将其输出为空,/dev/null 表示空