LaTeX 的报错难读,是因为它不是调用栈。! Undefined control sequence 下面那两行并不是在解释哪条命令写错了,而是 TeX 读取指针停下那一刻的快照——上面一行是已经读完的部分,下面一行是还没读的部分,两者之间的断口就是事故现场。想通这一点,同一套机制就能解释:为什么 l.NN 的行号有时会骗人,在 ? 提示符下该敲 h 还是 x,以及为什么 .log 里写着比终端更多的内容。本页讲报错信息的结构、-file-line-error、-interaction 的四种模式、日志的读法,以及如何用二分法把问题逼到一行。
报错信息的结构:! 那一行与上下两段的 l.NN
! 那一行说明发生了什么,从 l.NN 开始的上下两段说明在哪里停下——而元凶几乎总是在上半段的最右端。 TeX 把输入行切成「已读」和「未读」两部分,上下叠放,用缩进标出切口。下面的例子里,上半段末尾是 \textbnf,正是读到就出事的那条命令;{bold} text. 还没被处理,所以留在下半段。这个切口比行号可靠得多:行号是 TeX 发现问题的地方,切口是 TeX 当时所在的地方。
! 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},请往上看一行。
% \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 的上下两段照样打印,什么也没损失。
$ 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)。如果长文档里可能还有别的错误,最快的做法是敲 r 或 s 让它跑完,再回头读 .log。
? ?
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 列出四个取值——batchmode、nonstopmode、scrollmode、errorstopmode——默认是 errorstopmode。脚本驱动时用 -interaction=nonstopmode,CI 里不想刷屏就用 -interaction=batchmode。 区分这四者的只有两个维度:会不会停,以及会不会往终端写。最容易被误解的是 scrollmode 与 nonstopmode 的区别。实测:一个调用 \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。这与模式无关,nonstopmode 和 batchmode 一样,而且警告绝不改变退出码。因此 Makefile 或 CI 里写 pdflatex && … 时,只有错误会让它停下。
# 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.,而日志会具体写出该宏包最初是用哪些选项加载的、现在又请求了什么。有没有这四行,是「猜」和「知道」的区别。
% 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 上,它会把输出削减到只剩错误、警告和最后的总结行。
$ 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.cls、size10.clo、l3backend-pdftex.def)。只添一行 hyperref,就变成 33 个——也就是说 hyperref 一个人就拖来 30 个。tikz 是 34 个。当发现冲突里牵扯到一个自己从没加载过的宏包时,第一步就该敲这个。 发帖提问或提交缺陷报告时贴上这张表,两台机器的差异也能一眼看出来。
% \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 风格的调试用前者更好读,想在页码旁边打个记号则用后者更方便。
\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}。
\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,最后剩下的通常也就十来行。到了这个体量,原因一般已经一目了然;即便还看不出来,这十来行正好就是可以贴进提问里的最小示例。削减本身就是诊断——至于提问的礼节和该去哪儿问,由「社区」那一页负责。