错误调试
使用 Emacs 时难免会遇到插件不完善、不兼容导致的报错。报错默认只会在 *Message*
buffer 中显示报错信息,不会显示报错位置和调用栈,因此很难找到头绪。同时我们可能也会自己编写函数,编写的过程中同样需要进行 debug。本文简单介绍几个遇到问题的 debug 手法。
函数 debug
我们可以定义一个示例函数用于学习 Emacs 的 debugger。按下 C-x b
scratch
<RET>
切换到 *scratch*
buffer,我们编写一个用于开根的函数,但加入一个 bug:
1 (defun demo-sqrt (x)
2 (interactive "nGoing to calculate sqrt(x), please enter x: ")
3 (if (<= x 0)
4 (error (format "cannot calculate sqrt(%d) because the number is negative" x)))
5 (message "sqrt(%d)=%d" x (sqrt x)))
这个函数的期望功能是:接收用户输入的一个数字,如果这个数字是非负数,就打印出它的开根,否则报错。例如,我们按下 M-x eval-buffer
让函数生效,调用 M-x demo-sqrt <RET> 16 <RET>
,输出 "sqrt(16)=4"。
但第三行中,对非负数的判断有误,导致输入 0 时,也会报错。即如果输入为: M-x demo-sqrt <RET> 0 <RET>
,输出为 "cannot calculate sqrt(0) because the number is negative" (同时这个信息也会显示在 *Message*
中)。 这就是这个函数的一个 bug。
想要 debug 这个函数,我们首先加入一个函数断点: M-x debug-on-entry <RET> demo-sqrt <RET>
。表示在调用 demo-sqrt
这个函数时,进入 debugger。这时我们再次调用 M-x demo-sqrt <RET> 0 <RET>~,会发现进入了 ~*Backtrace*
buffer 中。
*Backtrace*
中每一行就是一级调用(frame)。按下 ?
可以打开帮助页面。其中写了几个主要操作。我们常用的有:
n
(backtrace-forward-frame
) 光标向下移动一个 frame,和C-n
相同,只是简化了操作。p
(backtrace-backward-frame
) 光标向上移动一个 frame,和C-p
相同,只是简化了操作。c
(debugger-continue
) 类似于常规 debugger 的 step over,会让光标所在位置的语句运行结束。d
(debuger-step-through
) 类似于常规 debugger 的 step in,可进入函数内调试内部逻辑。e
(eval-expression
) 和常规 debbugger 的 evaluate 相同,可以运行一个自定义语句。q
(debugger-quit
) 退出。
运行报错 debug
如果在调用一个函数时内部的某处逻辑发生了报错,默认情况下,并不会显示函数调用栈,因此较难定位问题。例如,我们编写这样的示例代码:
1 (defun demo-error-happens ()
2 (error "Hello but some error happens"))
3
4 (defun demo-hello-error ()
5 (interactive)
6 (demo-error-happens))
让函数生效 M-x eval-buffer
后,调用: M-x demo-hello-error
,这时下方的 Echo Area 显示 "Hello but some error happens" 。同时打开 *Message*
可以看到一条信息:"demo-error-happens: Hello but some error happens" 。尽管显示了调用函数的名字,但如果调用栈很深很复杂,仍无法快速找到报错位置了。
为了能够 debug 这个函数,我们可以让 Emacs 在报错时进入 debug 模式: M-x toggle-debug-on-error
,Echo Area 显示 "Debug on Error enabled globally",此时再次调用这个报错函数 M-x demo-hello-error
,会显示完整的调用栈 *Backtrace*
。为了便于解释,笔者添加了行号,而其本身是没有行号的:
1 1 Debugger entered--Lisp error: (error "Hello but some error happens")
2 2 signal(error ("Hello but some error happens"))
3 3 error("Hello but some error happens")
4 4 demo-error-happens()
5 5 demo-hello-error()
6 6 funcall-interactively(demo-hello-error)
7 7 call-interactively(demo-hello-error record nil)
8 8 command-execute(demo-hello-error record)
9 9 counsel-M-x-action("demo-hello-error")
10 ;; ...
11 10 command-execute(counsel-M-x)
可以看到,根据前面的教程,笔者安装了 counsel
,所以 M-x
其实调用的是 counsel-M-x
这个函数,然后这个函数最终调用了我们定义的 demo-hello-error
函数(第 9 行)。由于 counsel
有一些封装,实际直到第 5 行我们的函数才真正开始执行。在执行到 demo-error-happens
函数时(第 4 行),发生了第 3 行的报错信息。
每一行 Backtrace 都可以直接链接到源码。但此处由于我们是在 *scratch*
中编写的函数,Emacs 不会做跳转。其它正常在配置文件和插件(即常规 .el
文件)中定义的函数都是可以直接跳转的。由此我们就可以排查问题。
初始化报错 debug
有时我们在编写 ~/.emacs.d/init.el
、初始化配置或安装了部分插件时,可能会出现报错导致配置项无法正确加载。此时我们有多种方式可以快速定位。
同样的,我们在 ~/.emacs.d/init.el
中植入一个报错,在任何位置添加:
1 (error "Error happens on init")
随后与上文类似,我们可以打开 debug 模式。在启动 Emacs 时添加一个命令行参数:
1 $ emacs --debug-init
这时遇到报错时,同样会显示完整的调用栈。然后就可以正常 debug。
为了能够便于隔离问题、解决并验证问题,我们可以编写一个单独的 tmp-init.el
文件,然后使用这个文件作为初始化文件:
1 $ emacs -q --load tmp-init.el
-q
表示不要加载正常的初始化文件(也就是 ~/.emacs.d/init.el
、 ~/.emacs
等文件),~–load~ 表示随后加载另一个 Emacs Lisp 文件。这样,我们就可以在 tmp-init.el
中自由地尝试。
也可指定一个新的目录 /path/to/another/emacs.d
作为初始化目录,而不是 ~/.emacs.d/
:
1 $ emacs --init-directory=/path/to/another/emacs.d