# 第三章 Vim 常用命令 ## 3.6\* 调试命令 对任何一门语言,都有必要掌握调试技巧或手段。本节介绍 VimL 语言编程可以怎么调试 ,介绍一些自己的经验与体会。 ### echo 大法 对于不太庞大的程序或脚本,在关键疑点处打印消息都是简单方便的发现问题的手段,姑 且也算一种调试方法吧。 不过这明显有个问题,当程序调试完毕后,这些只为调试用的 `echo` 打印命令留着很碍 事呀,可能会与正常的输出混杂在一起,干扰正常结果呢。所以最好是能将正常的 `echo` 与调试的临时 `echo` 区分开来。正好,VimL 有个奇葩规定,在每行行语句之前的 `:` 冒号是可选的。这是为了与命令行表观上一致,然而正常的 vim 脚本一般都不会自找麻 烦多加这个冒号。但是若按语法规则,你在每行语句之前加一个冒号(甚至多个冒号)都 是没有关系的。 于是,不妨自己规范一下,将调试用的打印语句,都写成 `:echo`,或者喜欢多个空格 `: echo` 也行。而在正常的程序输出语句中,则用整洁的无冒号 `echo` 版。这样,当 调试完毕,确认程序无误后,就可以用 vim 强大的编辑命令将这些调试命令都删了: ```vim : g/:\s*echo/delete ``` 当然,你也许并不是想彻底删除,只是想注释掉,那就可用替换命令: ```vim : g/:\s*echo/s/:\s*echo/" echo/ ``` 当 `:s` 命令使用的正则表达式与前面的 `:g` 命令的正则表达式是一样的时候,可以简 写成 `: g/:\s*echo/s//" echo/`。因为 `:s//{replace}/` 命令中,空模式的意图是重 复使用上次的模式(寄存器 `/` 的内容)。若是为达这个目的,直接用替换命令也可以 的:`: %s/:\s*echo/" echo/`。不过与 `:g` 命令联用(先查找目标行,再替换)会更 灵活点,比如想将首列替换为注释符 `"`,而不影响内缩进的 `:echo` 命令,则可使用 这样的替换命令: ```vim : 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`), 然后实际的函数名是 `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),中断,类似 `` * interrupt (i), 也类似 `` * next (n),单步执行,类似 step over,会跳过函数调用与加载文件。 * step (s),单独执行,类似 step in,会步进函数调用或加载文件。 * finish (f),结束当前加载脚本或函数调用,回到调用处。 * backtrace (bt) 或 where,显示调用堆栈。 * frame (fr) {N} ,切换到堆栈的第 N 层,可用 `+` `-` 表示相对层。 * up / donw, 在堆栈处上移一层(`fr +1`)或下移一层(`fr -1`)。 以上这些调试命令可以尽可能缩写,只要前缀字符不冲突(小括号里也已标出最简缩写) 。直接敲回车表示重复上一次命令,这样就不必每次输入 `s` 或 `n` 命令了。 调试命令没有补全功能,只有普通 ex 命令才能补全。如果要使用与调试命令相同的普通 ex 命令,多加一个冒号,如 `:next`。但是,由于在 Ex 模式,编辑窗口是不更新的( 事实上,只要调试过程稍长,vim 窗口就完全被调试信息覆盖了),很多普通 ex 命令是 没有效果后,只有在完成调试模式后重回普通模式才能反映编辑窗口的变化。 真正有价值的 ex 命令是可用 `echo` 命令查看变量值,并且能根据当前环境查看相应作用 域的变量值,比如在加载脚本时可查看 `s:var`,运行到函数内部可看局部变量 `l:var` (在函数内默认局部变量,`:echo var` 就相当于 `:echo l:var`)。而在正常的命令行 下面,是无法查看 `s:var` 与 `l:var` 变量的。 在调试模式中,只能打印出正要执行的那行的源代码。这是典型的命令行式的调试方式, 并不能像 IDE 那般分裂出源码窗口,直接将光标定位到正在执行的行上。如果想查看完 整代码,只能用另外一个 vim 打开源文件查看了(有可能出现 `*.swp` 冲突问题,用只 读模式打开就好)。所以 VimL 调试的可视化程序仍稍嫌不足,希望日后还有改进。