阅读错误与调试

LaTeX 的报错难读,是因为它不是调用栈! Undefined control sequence 下面那两行并不是在解释哪条命令写错了,而是 TeX 读取指针停下那一刻的快照——上面一行是已经读完的部分,下面一行是还没读的部分,两者之间的断口就是事故现场。想通这一点,同一套机制就能解释:为什么 l.NN 的行号有时会骗人,在 ? 提示符下该敲 h 还是 x,以及为什么 .log 里写着比终端更多的内容。本页讲报错信息的结构、-file-line-error-interaction 的四种模式、日志的读法,以及如何用二分法把问题逼到一行。

报错信息的结构:! 那一行与上下两段的 l.NN

! 那一行说明发生了什么,从 l.NN 开始的上下两段说明在哪里停下——而元凶几乎总是在上半段的最右端。 TeX 把输入行切成「已读」和「未读」两部分,上下叠放,用缩进标出切口。下面的例子里,上半段末尾是 \textbnf,正是读到就出事的那条命令;{bold} text. 还没被处理,所以留在下半段。这个切口比行号可靠得多:行号是 TeX 发现问题的地方,切口是 TeX 当时所在的地方。

terminal
! Undefined control sequence.
l.3 This is \textbnf
                    {bold} text.
? 

l.NN 上方有时还会插入别的行,这就是错误的上下文。含有 -> 的行,例如 \mynorm #1->\lVert,表示故障发生在那条宏的展开过程中;<inserted text> 是 TeX 为了恢复自己补上的记号;<to be read again> 是读进来又被推回去的记号;<read *> 表示它正在等待终端输入。行太长放不下终端时,开头会用 ... 省略,所以看到 l.9 ...: $\frac{1}{2}$ and a stray \undefinedmacro 这样的显示,要想到实际的行首还在更左边。

上下文行含义
l.NN正在读取的输入行;上下两段的切口就是停下的位置
\mac #1->发生在 \mac 的展开过程中;定义在别处
<inserted text>TeX 为恢复而自行补入的记号,常常是 $
<recently read>刚刚读入的记号,通常本身就是原因
<to be read again>读入后又被推回的记号,接下来会被重新读取
<argument>发生在参数内部;要看参数而不是调用处
<read *>正在等待终端输入;非交互模式会当场中止

l.NN 为什么有时正好多出一行

\usepackage 引发的错误,通常会比实际位置晚报一行,原因是 \usepackage 末尾允许写一个可选的日期参数。 因为 \usepackage[opt]{pkg}[2021/02/14] 是合法写法,TeX 读到右花括号后必须再往后看一眼,确认后面是否跟着 [;这次前瞻会跳过空格和换行,于是在报错时它已经读进了下一行。在 TeX Live 2024 上实测:把 \usepackage[latin1]{inputenc} 写在第 3 行,选项冲突报在 l.4;在同一行末尾补上 [2021/02/14],报告就变成 l.3因此,如果与宏包有关的错误指向空行或 \begin{document},请往上看一行。

terminal
% \usepackage[latin1]{inputenc} sits on line 3 -- reported at line 4
./oc1.tex:4: LaTeX Error: Option clash for package inputenc.
l.4 \begin
          {document}

% same source, but the optional date argument is written out
% -- the look-ahead ends on line 3, and so does the report
./ocA.tex:3: LaTeX Error: Option clash for package inputenc.
l.3 \usepackage[latin1]{inputenc}[2021/02/14]

「发现的位置」与「出错的位置」之间的这种落差,在忘记闭合 } 时同样存在,只不过差距可能是几十行而不是一行,TeX 往往拖到段落结束或 \end{document} 才认输。具体病例——数学模式漏掉、命令未定义、花括号缺失——各有专页。这里只需记住一条通则:报出的行看起来越无辜,真正的毛病就越靠上游。

-file-line-error:让编辑器能跳转的格式

加上 -file-line-error,开头的 ! 就会换成 ./file.tex:3:,把文件名和行号放进同一行,编辑器或 CI 日志解析器可以直接跳过去。 默认格式有个实实在在的缺口:l.3 只给出数字,文件名要从上方很远处的 (./chapters/intro.tex 这样的左括号里去推断。对用 \input 拆成多章的文档来说,这一步推断最耗时间。-file-line-error 把它去掉,而 l.NN 的上下两段照样打印,什么也没损失。

terminal
$ pdflatex main.tex
(./chapters/intro.tex
! Undefined control sequence.
l.2 Here is \nosuchcmd
                       in a chapter.

$ pdflatex -file-line-error main.tex
(./chapters/intro.tex
./chapters/intro.tex:2: Undefined control sequence.
l.2 Here is \nosuchcmd
                       in a chapter.

在很多环境里,这个格式本来就是默认的:latexmk 会在内部打开它,TeXworks、VS Code 的 LaTeX Workshop 之类的前端也会替你加上。手工调用引擎时传 -file-line-error,想显式关闭就传 -no-file-line-error。还有一个有用的副作用:当错误来自宏包时,显示的路径就是那个宏包自己的文件——看到 /usr/local/texlive/…/foo.sty:120:,说明抱怨来自 foo,不是你写的东西有问题。

? 提示符的回答:h、i、x、q、r、s 和回车

? 提示符下有九种可用的回答,敲一个 ?,TeX 自己就会把清单打出来。 这是默认的 errorstopmode 的行为,此时 TeX 正在问你要怎么办。日常最常用的是三种:回车(忽略这条错误继续)、h(显示 TeX 自带的这条消息的帮助段落)、x(立即放弃,不生成 PDF)。如果长文档里可能还有别的错误,最快的做法是敲 rs 让它跑完,再回头读 .log

terminal
? ?
Type <return> to proceed, S to scroll future error messages,
R to run without stopping, Q to run quietly,
I to insert something, E to edit your file,
1 or ... or 9 to ignore the next 1 to 9 tokens of input,
H for help, X to quit.
?
回答TeX 会做什么
Return当这条错误没发生过,继续;排版照常进行,PDF 照样生成
h打印这条消息专属的帮助段落;.log 里本来就有
i在该处插入文本——i\textbf 只为这一次运行修正拼写
x立刻中止;输出 No pages of output.,不写 PDF
q显示 OK, entering \batchmode,此后静默跑完
r显示 OK, entering \nonstopmode...,此后一路不停跑完
s显示 OK, entering \scrollmode...;不停顿,但仍会读取终端输入
e用环境变量 TEXEDIT 指定的编辑器打开该行
1 … 9丢弃接下来的 1 至 9 个记号并继续;会在新的切口处重新显示该行

-interaction 的四种模式,各在什么时候用

pdflatex --help 列出四个取值——batchmodenonstopmodescrollmodeerrorstopmode——默认是 errorstopmode。脚本驱动时用 -interaction=nonstopmode,CI 里不想刷屏就用 -interaction=batchmode 区分这四者的只有两个维度:会不会停,以及会不会往终端写。最容易被误解的是 scrollmodenonstopmode 的区别。实测:一个调用 \typein 的文档,在 scrollmode 下确实会从终端读到答案,在 nonstopmode 下则以 ! Emergency stop. 结束。分界线不在错误,而在终端输入

模式是否停下、是否写终端
errorstopmode默认;每遇错误都停在 ? 提示符询问,适合手动排查
scrollmode遇错误不停,但仍读终端输入;适合把整次运行浏览一遍
nonstopmode完全不读终端;一旦有东西索要输入,就以 ! Emergency stop. 结束
batchmode在 nonstopmode 基础上再关掉终端输出;.log 仍然完整写出

batchmode「什么都不输出」,准确地讲是几乎什么都不输出。在 TeX Live 2024 上跑同一份含错文档并测量:终端输出在 nonstopmode 下是 1212 字节,在 batchmode 下是 144 字节——剩下的只有 pdfTeX 的横幅和 entering extended mode,因为它们在交互模式生效之前就已打印。而 .log 两种情况下都是 4144 字节,逐字节相同,PDF 也照样生成。所以 batchmode 并没有丢弃信息,只是不往终端流。CI 的标准做法由此而来:用批处理模式跑,用下一节的退出码判断成败,再收走 .log 看细节。这四个名字既是命令行选项也是 TeX 原语,因此在文件开头写 \nonstopmode 效果相同。

-halt-on-error 与退出码

-halt-on-error 会在第一个错误处终止运行。 在 TeX Live 2024 上验证:紧接着第一条 ! Undefined control sequence 之后,它打印 ! ==> Fatal error occurred, no output PDF file produced! 并退出,不留下 PDF。同一份文档只用 -interaction=nonstopmode 时会报完全部四条错误并且照样写出 PDF,所以当一份坏文档绝不能看起来像构建成功时,就该用这个开关。退出码也实测过:只要有任何错误就是 1,一条都没有就是 0。这与模式无关,nonstopmodebatchmode 一样,而且警告绝不改变退出码。因此 Makefile 或 CI 里写 pdflatex && … 时,只有错误会让它停下。

shell
# interactive: stop at the first error and look at it
pdflatex -file-line-error -halt-on-error document.tex

# CI: quiet terminal, full log on disk, exit status decides
pdflatex -interaction=batchmode -halt-on-error -file-line-error document.tex
echo $?      # 1 if any error occurred, 0 if none

.log 的读法:里面比终端多得多

.log 里原样保存着终端没有打印的帮助段落,所以看不懂某条消息时,不必重现问题再敲 h,打开日志即可。 一次运行的实测:终端收到 938 字节,.log 有 3199 字节,差额大部分就是这段帮助文字。效果最明显的是选项冲突:终端只显示 ! LaTeX Error: Option clash for package inputenc.,而日志会具体写出该宏包最初是用哪些选项加载的、现在又请求了什么。有没有这四行,是「猜」和「知道」的区别。

log
% only the first line of this reaches the terminal
./oc1.tex:4: LaTeX Error: Option clash for package inputenc.
...
The package inputenc has already been loaded with options:
  [utf8]
There has now been an attempt to load it with options
  [latin1]
Adding the global options:
  utf8,latin1
to your \documentclass declaration may fix this.

把整份日志的结构记下来同样划算。第一行给出引擎名、版本和运行的日期时间;下一行是启动参数 **document.tex;再往后全是嵌套的括号——( 打开一个文件,) 关闭它,所以「这个宏包是被哪个文件拉进来的」,答案就在括号的嵌套里[1][2] 标记已输出的页面,末尾是 Here is how much of TeX's memory you used: 之后的内存统计,以及 Output written on document.pdf (1 page, 12817 bytes).。如果不想面对这一整套,可以把运行接到 TeX Live 自带的 texfot 上,它会把输出削减到只剩错误、警告和最后的总结行。

terminal
$ texfot pdflatex -interaction=nonstopmode two.tex
texfot: invoking: pdflatex -interaction=nonstopmode two.tex
This is pdfTeX, Version 3.141592653-2.6-1.40.26 (TeX Live 2024)
! Undefined control sequence.
l.3 First \foo
! Missing $ inserted.
<inserted text>
l.4 Second \bar
Output written on two.pdf (1 page, 23091 bytes).

\listfiles*File List*:数清到底加载了什么

在导言区任意位置加一行 \listfiles.log 末尾就会多出一张 *File List* 表,把加载过的每个文件连同日期、版本和一行说明一起列出来。 在 TeX Live 2024 上数:一个光秃秃的 article 加载 3 个文件(article.clssize10.clol3backend-pdftex.def)。只添一行 hyperref,就变成 33 个——也就是说 hyperref 一个人就拖来 30 个。tikz 是 34 个。当发现冲突里牵扯到一个自己从没加载过的宏包时,第一步就该敲这个。 发帖提问或提交缺陷报告时贴上这张表,两台机器的差异也能一眼看出来。

log
% \listfiles goes anywhere in the preamble; this lands at the end of the .log
 *File List*
 article.cls    2023/05/17 v1.4n Standard LaTeX document class
  size10.clo    2023/05/17 v1.4n Standard LaTeX file (size option)
 amsmath.sty    2023/05/13 v2.17o AMS math features
hyperref.sty    2024-01-20 v7.01h Hypertext links for LaTeX
   iftex.sty    2022/02/03 v1.0f TeX engine tests
 ***********

想看得更细就加上 -recorder。运行期间打开的每个文件都会以 INPUT 行写进 .fls;一个只加载了 tikz 的文档就有 140 行。\listfiles 回答的是「加载了哪些宏包」,.fls 回答的是「碰过哪些文件」,连字体的 .tfm 和配置文件都算在内。追查宏包冲突用前者,追查 kpathsea 到底去哪儿找过用后者。

\show / \showthe / \typeout:把 TeX 心里想的打出来

\show\foo 打印 \foo 的定义,\showthe\textwidth 打印长度或计数器的值。 输出会以 > \LaTeX=macro:> 345.0pt. 的形式留在 .log 里,行首的 > 就是标记(顺带一提,345.0pt 正是 article 的默认 \textwidth)。记不清某条命令当前是怎么定义的时候,\show 胜过猜测,而且通常能判明重定义来自文档类还是某个宏包。要输出自己的消息有 \typeout{…}\message{…};实测下来,\typeout 会另起一行,\message 则接在当前行后面。printf 风格的调试用前者更好读,想在页码旁边打个记号则用后者更方便。

latex
\show\LaTeX            % > \LaTeX=macro:  ... (definition follows)
\showthe\textwidth     % > 345.0pt.       (article default)
\typeout{reached the theorem}   % own line in log and terminal
\message{mark}                  % appended to the current line
\tracingall            % dump every step to the log -- extremely verbose

最后的手段是 \tracingall,它会把 TeX 做的每一步——宏展开、模式切换、断行尝试——都写进日志。几页的文档就可能产生几十 MB,所以原则上要在问题发生前一刻打开、发生后一刻用 \tracingnone 关掉,或者配合 trace 宏包使用(它会把输出整理得更好读)。\tracingall 回答的是「事情按什么顺序发生」,而不是「哪条宏在捣乱」——顺序一旦清楚,剩下的通常靠一句 \show 就能定案。

用二分法逼出元凶:把 \end{document} 往上挪

光看消息判断不出原因时,把文档砍成一半是最短的路:在正文中间再写一个 \end{document},它后面的全部内容都会被忽略。 在 TeX Live 2024 上验证过——\end{document} 之后无论写了什么,哪怕是坏掉的命令,都不会被读取。所以连原来那个都不必删,只要把新加的这一行上下挪动,就能两头夹逼。挪十次,就能把一千行的文档收敛到一行。如果怀疑导言区,就每次用 % 注释掉一半 \usepackage;如果用 \include 分了章节,就改用 \includeonly{chapter3}

document.tex
\begin{document}
\input{chapters/intro}
\input{chapters/method}

\end{document}   % <- added: bisect here, everything below is ignored

\input{chapters/results}
\input{chapters/discussion}
\end{document}

缩到一半之后,就继续削到还能复现问题的最小形态。一条条去掉 \usepackage,一段段丢掉正文,把插图换成 graphicx 自带的 example-image,把长段落换成 lipsum,最后剩下的通常也就十来行。到了这个体量,原因一般已经一目了然;即便还看不出来,这十来行正好就是可以贴进提问里的最小示例。削减本身就是诊断——至于提问的礼节和该去哪儿问,由「社区」那一页负责。