Contents

调试Bash脚本

1. 概述

在本教程中,我们将了解调试 Bash shell 脚本的各种技术。Bash shell 不提供任何内置调试器。但是,有一些命令和结构可用于此目的。

首先,我们将讨论set 命令调试脚本的用法。之后,我们将使用settrap 命令检查一些调试特定用例。最后,我们将介绍一些调试已运行脚本的方法。

2. Bash 调试选项

Bash shell 中可用的调试选项可以通过多种方式打开和关闭。在脚本中,我们可以使用set命令或向shebang 行添加一个选项。但是,另一种方法是在执行脚本时在命令行中显式指定调试选项

让我们深入讨论。

2.1. 启用详细模式

我们可以使用-v 开关启用详细模式,这允许我们在执行每个命令之前查看它。**

为了演示这一点,让我们创建一个示例脚本:

#! /bin/bash
read -p "Enter the input: " val
zero_val=0
if [ "$val" -gt "$zero_val" ]
then
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi

此脚本检查作为输入输入的数字是否为正数。

接下来,让我们执行我们的脚本:

$ bash -v ./positive_check.sh
#! /bin/bash
read -p "Enter the input: " val
Enter the input: -10
zero_val=0
if [ "$val" -gt "$zero_val" ]
then
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi
The input value is not positive.

我们可以注意到,它在处理之前在终端上打印脚本的每一行。

我们还可以在shebang行中添加*-v*选项:

#! /bin/bash -v

这与使用bash -v显式调用脚本的效果相同。另一个等价物是使用set命令在脚本中启用该模式:

#! /bin/bash
set -v

事实上,我们可以使用上面讨论的任何一种方式来启用我们将在以后讨论的各种开关。

2.2. 使用noexec模式进行语法检查

在某些情况下,我们可能希望在脚本执行之前对其进行语法验证。如果是这样**,我们可以使用*-n选项使用noexec*模式**。结果,Bash 将读取命令但不执行它们。

让我们在noexec模式下执行我们的positive_check.sh脚本:

$ bash -n ./positive_check.sh

这会产生空白输出,因为没有语法错误。现在,我们将稍微修改一下脚本并删除then语句:

#! /bin/bash
read -p "Enter the input: " val
zero_val=0
if [ "$val" -gt "$zero_val" ]
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi

接下来,我们将使用*-n*选项在语法上验证它:

$ bash -n ./positive_check_noexec.sh
./positive_check_noexec.sh: line 6: syntax error near unexpected token `else'
./positive_check_noexec.sh: line 6: `  else'

正如预期的那样,它抛出了一个错误,因为我们错过了if 条件中的then 语句。

2.3. 使用xtrace模式进行调试

在上一节中,我们测试了脚本的语法错误。但是为了识别逻辑错误,我们可能希望在执行过程中跟踪变量和命令的状态。在这种情况下,我们可以使用-x选项在xtrace(执行跟踪)模式下执行脚本。

此模式在展开之后但在执行之前打印每行的命令跟踪

让我们在执行跟踪模式下执行我们的positive_check.sh脚本:

$ bash -x ./positive_check.sh
+ read -p 'Enter the input: ' val
Enter the input: 17
+ zero_val=0
+ '[' 17 -gt 0 ']'
+ echo 'Positive number entered.'
Positive number entered.

在这里,我们可以在执行前在stdout 上看到变量的扩展版本。需要注意的是,前面有*+号的行是由xtrace*模式生成的。

2.4. 识别未设置的变量

让我们运行一个实验来了解 Bash 脚本中未设置变量的默认行为:

#! /bin/bash
five_val=5
two_val=2
total=$((five_val+tow_val))
echo $total

我们现在将执行上面的脚本:

$ ./add_values.sh
5

我们可以注意到,有一个问题:脚本执行成功,但输出逻辑不正确。

我们现在将使用*-u*选项执行脚本:

$ bash -u ./add_values.sh
./add_values.sh: line 4: tow_val: unbound variable

当然,现在更加清晰了!

由于未定义变量tow_val ,脚本无法执行。我们在计算 total时错误地将two_val输入为tow_val

** -u选项在执行参数扩展时将未设置的变量和参数视为错误**。因此,在使用*-u*选项执行脚本时,我们会收到一个错误通知,指出变量未绑定到值

3. 调试 Shell 脚本的用例

到目前为止,我们看到了调试脚本的各种开关。此后,我们将研究一些用例和方法来在 shell 脚本中实现这些。

3.1.组合调试选项

为了获得更好的见解,我们可以进一步组合set命令的各种选项

让我们在启用*-v-u选项的情况下执行add_values.sh*脚本:

$ bash -uv ./add_values.sh
#! /bin/bash
five_val=5
two_val=2
total=$((five_val+tow_val))
./add_values.sh: line 4: tow_val: unbound variable

在这里,通过使用*-u选项启用详细*模式,我们可以轻松识别触发错误的语句。

同样,我们可以结合verbosextrace模式来获得更精确的调试信息

如前所述,-v选项在评估之前显示每一行,而*-xo选项在展开之后显示每一行。因此,*我们可以结合使用-x和*-v*选项来查看语句在变量替换之前和之后的样子。**

现在,让我们在启用*-x-v模式的情况下执行positive_check.sh*脚本:

$ bash -xv ./positive_check.sh
#! /bin/bash
read -p "Enter the input: " val
+ read -p 'Enter the input: ' val
Enter the input: 5
zero_val=0
+ zero_val=0
if [ "$val" -gt "$zero_val" ]
then
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi
+ '[' 5 -gt 0 ']'
+ echo 'Positive number entered.'
Positive number entered.

我们可以观察到语句在变量扩展之前和之后打印在stdout上。

3.2. 调试脚本的特定部分

使用*-x-v选项 shell 脚本进行调试会为stdout上的每个语句生成一个输出。但是,在某些情况下,我们可能希望仅将调试信息减少到脚本的特定部分。我们可以通过在代码块启动之前启用调试模式来实现这一点,然后使用set*命令将其重置。

让我们用一个例子来检查一下:

#! /bin/bash
read -p "Enter the input: " val
zero_val=0
set -x
if [ "$val" -gt "$zero_val" ]
then
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi
set +x
echo "Script Ended"
 

在这里,我们可以在条件开始之前使用set语句仅调试if条件。稍后,我们可以在if块结束后使用set +x命令重置xtrace模式。

让我们用输出验证它:

$ ./positive_debug.sh
Enter the input: 7
+ '[' 7 -gt 0 ']'
+ echo 'Positive number entered.'
Positive number entered.
+ set +x
Script Ended

当然,输出看起来不那么混乱。

3.3. 仅将调试输出重定向到文件

在上一节中,我们研究了如何将调试限制在脚本的某些部分。因此,我们可以限制stdout的输出量。

此外,我们可以将调试信息重定向到另一个文件,并让脚本输出打印在stdout上。

让我们创建另一个脚本来检查它:

#! /bin/bash
exec 5> debug.log 
PS4='$LINENO: ' 
BASH_XTRACEFD="5" 
read -p "Enter the input: " val
zero_val=0
if [ "$val" -gt "$zero_val" ]
then
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi

首先,我们使用exec 命令打开文件描述符 (FD) 5上的debug.log文件以进行写入。

然后我们更改了特殊的 shell 变量 PS4 。PS4变量定义了当我们在xtrace模式下执行 shell 脚本时显示的提示。

PS4的  默认值为*+。我们更改了PS4变量的值以在调试提示中显示行号。为此,我们使用了另一个特殊的 shell 变量LINENO*。

后来,我们将 FD 5 分配给 Bash 变量BASH_XTRACEFD 。实际上,Bash 现在会将xtrace输出写入 FD5 即debug.log文件。让我们执行脚本:

$ bash -x ./debug_logging.sh
+ exec
+ PS4='$LINENO: '
4: BASH_XTRACEFD=5
Enter the input: 2
Positive number entered.

正如预期的那样,调试输出没有写入终端。虽然,前几行,直到 FD 5被分配给调试输出被打印。

此外,该脚本还会创建一个输出文件debug.log,其中包含调试信息:

$ cat debug.log
5: read -p 'Enter the input: ' val
6: zero_val=0
7: '[' 2 -gt 0 ']'
9: echo 'Positive number entered.'

4. 使用trap调试脚本

我们可以利用 Bash 的DEBUGtrap 功能重复执行命令。

trap命令的参数中指定的命令在脚本中的每个后续语句之前执行。

让我们用一个例子来说明这一点:

#! /bin/bash
trap 'echo "Line- ${LINENO}: five_val=${five_val}, two_val=${two_val}, total=${total}" ' DEBUG
five_val=5
two_val=2
total=$((five_val+two_val))
echo "Total is: $total"
total=0 && echo "Resetting Total"

在这个例子中,我们指定了echo命令来打印变量  Five_valtwo_valtotal 的值。随后,我们将这个 echo 语句传递给带有DEBUG信号的trap命令。实际上,在执行脚本中的每个命令之前,都会打印变量的值。 让我们检查生成的输出:

$ ./trap_debug.sh
Line- 3: five_val=, two_val=, total=
Line- 4: five_val=5, two_val=, total=
Line- 5: five_val=5, two_val=2, total=
Line- 6: five_val=5, two_val=2, total=7
Total is: 7
Line- 7: five_val=5, two_val=2, total=7
Line- 7: five_val=5, two_val=2, total=0
Resetting Total

5. 调试已经运行的脚本

到目前为止,我们介绍了在执行 shell 脚本时调试它们的方法。现在,我们将研究调试已经运行的脚本的方法。

考虑一个在无限循环 中执行sleep 的示例运行脚本:

#! /bin/bash
while :
do
 sleep 10 &
 echo "Sleeping for 4 seconds.."
 sleep 4
done

pstree 命令的帮助下,我们可以检查脚本sleep.sh分叉的子进程

$ pstree -p
init(1)─┬─init(148)───bash(149)───sleep.sh(372)─┬─sleep(422)
        │                                        ├─sleep(424)
        │                                        └─sleep(425)
        ├─init(213)───bash(214)───pstree(426)
        └─{init}(7)

我们使用了一个附加选项*-p*来打印进程 ID 和进程名称。因此,我们能够意识到脚本正在等待子进程 ( sleep ) 完成。

有时我们可能想仔细查看我们的流程执行的操作。在这种情况下,我们可以使用strace 命令来跟踪正在进行的 Linux 系统调用

$ sudo strace -c -fp 372
strace: Process 372 attached
strace: Process 789 attached
strace: Process 790 attached
^Cstrace: Process 372 detached
strace: Process 789 detached
strace: Process 790 detached
## % time     seconds  usecs/call     calls    errors syscall
100.00    0.015625        5208         3           wait4
  0.00    0.000000           0         6           read
  0.00    0.000000           0         1           write
  0.00    0.000000           0        39           close
  0.00    0.000000           0        36           fstat
  0.00    0.000000           0        38           mmap
  0.00    0.000000           0         8           mprotect
  0.00    0.000000           0         2           munmap
  0.00    0.000000           0         6           brk
  0.00    0.000000           0        16           rt_sigaction
  0.00    0.000000           0        20           rt_sigprocmask
  0.00    0.000000           0         1           rt_sigreturn
  0.00    0.000000           0         6         6 access
  0.00    0.000000           0         1           dup2
  0.00    0.000000           0         2           getpid
  0.00    0.000000           0         2           clone
  0.00    0.000000           0         2           execve
  0.00    0.000000           0         2           arch_prctl
##   0.00    0.000000           0        37           openat
100.00    0.015625                   228         6 total

在这里,我们使用选项 -p 附加到进程 id (372)即我们正在执行的脚本。此外,我们还使用-f选项附加到其所有子进程。请注意,strace命令会为每个系统调用生成输出。因此,我们使用*-c选项在strace*终止时打印系统调用的摘要。