shell 的错误处理和调试方法

简介

    在我们写代码过程中,一般有两个阶段:调试阶段和试运行阶段。在调试阶段我们希望尽可能的输出日志,方便在出错的时候快速定位问题。在试运行阶段希望将日志标准化,且有些错误的日志是在预期内不想展示的时候如何处理,这篇基础文章将介绍这两个阶段如果有效的节约编程时间。

        

目录

1. 脚本调试

1.1. 日志输出

1.2. debug调试

2. 运行shell脚本的异常报错

2.1. 找不到命令

2.2. 语法缺少结束符

2.3. 部分命令无法执行(巨坑)

3. 错误处理

3.1. 异常状态码

3.2. 正常、异常日志重定向


        

1. 脚本调试

我们在编写脚本时,调试时需要用到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

在代码中,我们使用了时间+类型+信息的方式汇报结果,这可以使得我们对某个任务的执行时间和做的事情有很清晰的了解。

shell 的错误处理和调试方法_第1张图片

        

如果觉得每次输出日志都需要加一下时间之类的东西很麻烦,不妨试试用函数封装一个方法

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"

按照预期输出了时间、类型、字符串、结果。

        

但这还不够,我们再来改进一下:

  1. result 每次输入 SUCCEED 或 FAILED 太麻烦了,直接用 1 和 0 替代。
  2. result 需要支持 不输出结果、自定义结果、0或1选项。
  3. 如果 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

结果如下:

shell 的错误处理和调试方法_第2张图片

在执行任务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 等待,结果如下:刚开始两种同时监控,但后面只监控到了磁盘

shell 的错误处理和调试方法_第3张图片

当出现这种不符合预期的情况,系统也没有报错,那么我们需要查看 debug 日志:

shell 的错误处理和调试方法_第4张图片

在这张图片中,分别出现了2种不同类型的日志:带+号、不带+号。

  • 带+符号:表示脚本中的代码
  • 不带+号:表示输出的日志信息

在这些带+号的代码中,又分别会出现1个、2个、3个、n个,这些实际上是级别表示:

  • +:一级执行级别(顶层执行的命令,通常是整个脚本中的命令)。
  • ++:二级执行级别(通常用于嵌套在一级执行级别命令中的命令)。
  • +++:三级执行级别(更深度嵌套的代码或执行流程)。

我们来看一下这个脚本的信息

shell 的错误处理和调试方法_第5张图片

先看红框,这是两个任务的执行信息, 标注的1、2、3、4分别是它们的执行流程。

1:执行的内存监控

2:执行的磁盘监控

3:执行的磁盘监控

4:执行的磁盘监控

我们发现执行内存监控后就一直执行磁盘监控,而后内存监控没再工作。所以我们往1~3的中间找找其他日志,发现在2后面出现一个 wait 命令,使用 wait 后会持续等待子进程结束,所以,这个脚本的问题就在于 wait ,我们重新将函数和 wait 放入最后一行。

MonitorDisk &
MonitorMemory &
wait

最终结果:符合预期

shell 的错误处理和调试方法_第6张图片

        

2. 运行shell脚本的异常报错

shell 的运行有2个点需要注意:

  1. 如果脚本中出现语法不正确时并不会在执行前检查,而是在执行过程中发现语法错误后自动退出;
  2. 如果脚本中语法正确,但执行过程中的"命令"出错不会退出。

针对这两点我们来看看语法问题应该如何处理,有哪些坑需要注意。

  • Linux中可以通过命令 shellcheck [脚本] 来检查脚本语法,这里就不对这个命令进行说明了。

        

2.1. 找不到命令

  • 出现找不到命令的错误不会终止脚本,会继续执行。
echo "=========开始运行脚本========="
a    # 执行一个错误的命令
echo "=========结束运行脚本========="

我们设置了 3 条命令,开始和结尾的命令是正常的,中间命令是不存在的,看一下结果:

shell 的错误处理和调试方法_第7张图片

运行结果如下:

  • 【正常】执行第1条命令
  • 【异常】执行第2条命令
  • 【正常】执行第3条命令

中间出现了异常的命令,shell不会终止脚本,输出对应信息后继续往下执行。输出的信息:

  • 【脚本路径】【异常行数】【异常命令】【异常提示】

正常的处理流程就是:

  1. 查看异常提示是什么
  2. 查看异常行数,vim +[行号] [脚本] 检查问题
  3. 最后修改问题

        

2.2. 语法缺少结束符

  • 出现语法错误后终止脚本,但不会提前检查。
echo "=========开始运行脚本========="

if [ 1 -eq 1 ];then
    echo "正确"

echo "=========结束运行脚本========="

shell 的错误处理和调试方法_第8张图片

这里可以看到脚本的第1行正常执行,第2行的 if 判断因为语法问题而报错,报错以后直接退出,后面的代码不再执行。箭头处系统给出的报错文件是第9行,而我们文件总共才8行,哪来的第9行。我去查了一下资料没查到,有懂的小伙伴请评论区留言。所以我去总结了一下哪些情况会这样:

  • if 判断缺少结束符 fi
  • for 循环缺少结束符 done
  • while 循环缺少结束符 done
  • case 缺少结束符不会这样,会在缺少的那一行报错。

总的来说,只要看到报错的行数大于文件总行数,并且只输出了语法错误 没有具体的错误信息,那基本就是结尾符的问题了。

        

理解了这个异常是什么导致的以后,下一个问题来了,当我们脚本很大 又没有用函数封装,利用单行注释去调试又太慢,怎么办?

举个例子,这里有很多个 if

shell 的错误处理和调试方法_第9张图片

执行结果是这样的:前面正常执行,后面语法错误,抛出异常400行

shell 的错误处理和调试方法_第10张图片

我们用最简单的方法:过滤查找(缩小范围)

grep -nE "if|fi" [文件名]

使用 grep 输出包含 if 和 fi 的行号和信息,手动去检查,如果 if 下面缺少 fi 基本就能确定是哪行

shell 的错误处理和调试方法_第11张图片

13 和 17 行这里出现了2个 if ,一般嵌套很少有 if 嵌套 if,即使有也很少。通过这里我们发现 13 的 if 没有结束符,如果代码类似我这种情况2s搞定,如果比较复杂按缩进排查就行。

shell 的错误处理和调试方法_第12张图片

        

2.3. 部分命令无法执行(巨坑)

为什么说这个时巨坑,因为执行的时候不报错,bash -x 发现不了问题。这是之前在写一个shell过程中,拷贝代码过来导致中间一部分命令无法执行,找了半个小时才发现罪魁祸首是 EOF。

通过 <

#将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+ :通常表示命令或脚本因接收到异常信号而终止。

所以我们在判断一个命令是否执行成功,只需要使用 $?。例如执行一个异常的命令

shell 的错误处理和调试方法_第13张图片

返回的状态码非 0

        

再来执行一个正常的命令

shell 的错误处理和调试方法_第14张图片

返回状态码为 0

        

所以当我们判断一个命令是否执行成功时,可以这样写

ls "abc"
if [ $? -eq 0 ];then
    echo "状态码为0,上一条命令执行成功!"
else
    echo "状态码非0,上一条命令执行失败!"
fi

shell 的错误处理和调试方法_第15张图片

        

3.2. 正常、异常日志重定向

在 shell 中,系统抛出的日志分为正常和异常两种。当我们对某个命令所返回的结果重定向到另一个文件中时,系统会自动判断:如果命令执行成功则可以重定向到某个文件,如果命令执行失败则无法重定向到某个文件,直接输出到屏幕。

shell 的错误处理和调试方法_第16张图片

可以看到,执行成功的命令结果是可以重定向到文件 tmp.txt 中,而执行失败的结果是无法重定向到 tmp.txt 中。

        

如果我们必须将任务的执行结果输出到一个文件时(不论正常还是异常),那么可以通过 1 和 2 来指定

  • 0 :表示标准输入 stdin,通常对应于键盘输入。
  • 1 :表示标准输出 stdout,通常对应于命令或脚本的正常输出。
  • 2 :表示标准错误输出 stderr,通常用于输出命令或脚本的错误信息。

shell 的错误处理和调试方法_第17张图片

使用 2>&1 将标准错误输出 stderr 重定向到标准输出 stdout 上,所以可以输出到文件。

        

如果我们希望将错误日志和正常日志分开存放应该怎么处理呢?

shell 的错误处理和调试方法_第18张图片

使用 1 和 2 分开存放,将正常的日志追加到 info.log,将异常的日志追加到 err.log

        

如果不想输出日志又怎么处理呢?

shell 的错误处理和调试方法_第19张图片

直接将其输出为空,/dev/null 表示空

你可能感兴趣的:(shell,编程,linux,运维,shell)