3.6* 调试命令

优质
小牛编辑
125浏览
2023-12-01

3.6* 调试命令

对任何一门语言,都有必要掌握调试技巧或手段。本节介绍 VimL 语言编程可以怎么调试 ,介绍一些自己的经验与体会。

echo 大法

对于不太庞大的程序或脚本,在关键疑点处打印消息都是简单方便的发现问题的手段,姑 且也算一种调试方法吧。

不过这明显有个问题,当程序调试完毕后,这些只为调试用的 echo 打印命令留着很碍 事呀,可能会与正常的输出混杂在一起,干扰正常结果呢。所以最好是能将正常的 echo 与调试的临时 echo 区分开来。正好,VimL 有个奇葩规定,在每行行语句之前的 : 冒号是可选的。这是为了与命令行表观上一致,然而正常的 vim 脚本一般都不会自找麻 烦多加这个冒号。但是若按语法规则,你在每行语句之前加一个冒号(甚至多个冒号)都 是没有关系的。

于是,不妨自己规范一下,将调试用的打印语句,都写成 :echo,或者喜欢多个空格 : echo 也行。而在正常的程序输出语句中,则用整洁的无冒号 echo 版。这样,当 调试完毕,确认程序无误后,就可以用 vim 强大的编辑命令将这些调试命令都删了:

: g/:\s*echo/delete

当然,你也许并不是想彻底删除,只是想注释掉,那就可用替换命令:

: g/:\s*echo/s/:\s*echo/" echo/

:s 命令使用的正则表达式与前面的 :g 命令的正则表达式是一样的时候,可以简 写成 : g/:\s*echo/s//" echo/。因为 :s//{replace}/ 命令中,空模式的意图是重 复使用上次的模式(寄存器 / 的内容)。若是为达这个目的,直接用替换命令也可以 的:: %s/:\s*echo/" echo/。不过与 :g 命令联用(先查找目标行,再替换)会更 灵活点,比如想将首列替换为注释符 ",而不影响内缩进的 :echo 命令,则可使用 这样的替换命令:

: g/:\s*echo/s/^./"/

如果想更细致点,可以自行将 :echo::echo 用于不同场合,比如不同等级的调 试输出。

还有个问题,:echo 命令的输出是易逝的,后一批的命令(vim 的解释单元)输出会覆 盖掉前一批的命令输出。如果想保存这样的输入,有以下几种办法:

  • :echomsg 用这个命令替换 :echo,则输出信息会保存在消息区,以后可用 :message 再次查看,当消息区的信息比较多时,可能需要翻页查看,G 跳到最后 一页,基本上就是最近的输出了。
  • :redir 命令重定向,可以将随后的 :echo 消息重定向至文件、寄存器、变量中, 当然也会同时显示在屏幕上。不再需要重定向功能时用 :redir END 命令取消。
    • :redir! > {file} 重定向到文件中,当文件已存在时,用 ! 强制覆盖。
    • ':redir @{reg}>' 重定向至寄存器,如果支持系统剪贴板,用 *+ 表示。
    • :redir => {var} 重定义向至一个变量中。
    • :redir >> 将上述命令中的 > 换为 >> 表示附加。
  • &verbosfile 将详情信息写入这个选项值指定的文件中。&verbose 选项值设定详 情信息的等级。

断点进入调试模式

Vim 也提供了正式的调试模式,那有点像允许单步执行的 Ex 模式。一般需要先设置断点 ,随后当脚本运行到断点处,就进入了调试模式。添加断点用 :breakadd 命令:

  • :breakadd file [lnum] {name} 在一个 vim 脚本文件中的某行加断点,行号可选。 注意如果提供行号,行号参数位于文件名之前,如果省略行号,相当于第 1 行。随后 当 :source {name} 加载该脚本时,执行到那行时会暂停,进入调试模式。

  • :breakadd func [lnum] {name} 在某函数的第几行打断点。{name} 指函数名。如 果是全局函数,那就是直接的函数名,如 FuncName。如果是脚本局部函数,如 s:FuncName ,则要先找到那个脚本在当前 vim 会话的脚本号(:scriptnames), 然后实际的函数名是 <SNR>dd_FuncName,其中 dd 就是脚本号数字。如果是匿名 函数,它没有名字,就只能用其函数编号了,如 :breakadd func 1 21 表示在第 21 个匿名函数的第1行处打断点。那匿名函数编号如何确定呢?如果这个函数有出错了, 在错误信息中会打印出出错函数的名字与行号,匿名函数没名字就用编号代替了。(没 有出错么?没出错为啥调试?)至于 [lnum] 行号,可理解为函数体内相对于函数头 定义的相对行号,可不是该函数定义块在脚本文件中的行号。即从函数定义头按 [lnum]j 就是函数断点处。

  • :breakadd here 当你在编辑一个 vim 脚本文件时,相当于在当前文件的当前行加入 断点。如果你已经进入了调试模式,并且已经单步进入了某个函数,:breakadd here 也可以在当前函数的当前行加入断点,下次再次调用该函数时(或下次循环)运行到 此处时也会暂停。

当用 :breakadd 添加了一些断点后,可用 :breaklist 查看断点信息。也可用 :breakdel 删除断点。

  • :breakdel {nr} 按断点号删除某个断点(:breaklist 会列出断点号)。
  • :breakdel * 删除所有断点。
  • :breakdel file [lnum] {name}
  • :breakdel func [lnum] {name}
  • :breakdel here 这三个命令与 :breakadd 相似,但是删除断点。

除了通过 :breakadd 添加断点,以期将来运行到彼处时进入调试模式外,还有另外两 种方式直接进入调试模式:

  • :debug {cmd} 在执行命令之前附加 :debug ,就将在执行该命令时立即进入调试 模式,一般接着用 s (step in)深入调试,如果用 n (step over)可能就将整 条 {cmd} 命令当作一步直接执行完了,并不能达到调试效果。
  • vim -D {other args} 在启动 vim 时,通过 -D 命令行参数,直接在加载 vimrc 时就开始进入调试模式了。

调试模式

调试模式是一种特殊的 Ex 模式,除了一般的 ex 命令,还可以使用以下调试命令:

  • cont (c),表示继续执行,直到遇到下一断点,或结束。
  • quit (q),中断,类似 <Ctrl-C>
  • interrupt (i), 也类似 <Ctrl-C>
  • next (n),单步执行,类似 step over,会跳过函数调用与加载文件。
  • step (s),单独执行,类似 step in,会步进函数调用或加载文件。
  • finish (f),结束当前加载脚本或函数调用,回到调用处。
  • backtrace (bt) 或 where,显示调用堆栈。
  • frame (fr) {N} ,切换到堆栈的第 N 层,可用 + - 表示相对层。
  • up / donw, 在堆栈处上移一层(fr +1)或下移一层(fr -1)。

以上这些调试命令可以尽可能缩写,只要前缀字符不冲突(小括号里也已标出最简缩写) 。直接敲回车表示重复上一次命令,这样就不必每次输入 sn 命令了。

调试命令没有补全功能,只有普通 ex 命令才能补全。如果要使用与调试命令相同的普通 ex 命令,多加一个冒号,如 :next。但是,由于在 Ex 模式,编辑窗口是不更新的( 事实上,只要调试过程稍长,vim 窗口就完全被调试信息覆盖了),很多普通 ex 命令是 没有效果后,只有在完成调试模式后重回普通模式才能反映编辑窗口的变化。

真正有价值的 ex 命令是可用 echo 命令查看变量值,并且能根据当前环境查看相应作用 域的变量值,比如在加载脚本时可查看 s:var,运行到函数内部可看局部变量 l:var (在函数内默认局部变量,:echo var 就相当于 :echo l:var)。而在正常的命令行 下面,是无法查看 s:varl:var 变量的。

在调试模式中,只能打印出正要执行的那行的源代码。这是典型的命令行式的调试方式, 并不能像 IDE 那般分裂出源码窗口,直接将光标定位到正在执行的行上。如果想查看完 整代码,只能用另外一个 vim 打开源文件查看了(有可能出现 *.swp 冲突问题,用只 读模式打开就好)。所以 VimL 调试的可视化程序仍稍嫌不足,希望日后还有改进。