如何有效地追踪 LaTeX 错误?

如何有效地追踪 LaTeX 错误?

LaTeX 最令人讨厌的事情之一是处理错误可能很麻烦且令人伤脑筋。事实上,这是我不向大多数人(不仅仅是 Apple 用户)积极推荐 LaTeX 的主要原因。处理 LaTeX 错误不是普通用户能做到的事情。

我认为,主要原因是错误信息往往含糊不清,而且往往具有误导性。“含糊不清”的问题可以通过以下方法解决:学习它们的基本结构。但“误导”是一个真正的问题。

例如,昨天我花了一个多小时的时间处理各种错误消息,例如biber

Entity: line 2004: parser error : Extra content at the end of the document <bcf:citekey order="31">AuthorYear</bcf:citekey> ^

还有来自pdflatex,比如这个:

[9
! pdfTeX error (font expansion): auto expansion is only possible with scalable 
fonts.
\@EveryShipout@Output ...@Org@Shipout \box \@cclv 

l.987 

但我的问题并不是特别针对这些错误。它们只是说明这些错误描述是多么具有误导性。因为导致这一切的问题只是因为我写了

\textsf{bla bla bla}

代替

\enquote{bla bla bla}

我还要补充一点:那个错误出现在第 918 行,所以这些错误消息中指示的行号也相差甚远(好吧,biber不是指 .tex 文件,但我也必须了解这一点)。

我把导致这些错误消息的逻辑留给了极客们(因为我确信存在(隐藏的)逻辑)。但对于那些想知道我到底是如何混淆的人来说\textsf\enquote答案是:我没有输入它,我使用了(错误的)键盘快捷键。

现在,你可以说我愚蠢或随便什么。这只是一个例子。

我的观点是:在有效地编译 LaTeX 文档时,有哪些策略可以系统地追踪错误来源?

让我补充一个更具体的版本:假设一种策略是使用某种版本控制系统返回到文件的最后一个无错误版本并识别导致错误的更改,是否有任何自动或半自动的方法可以做到这一点。例如,我的文件在 Dropbox 上,这使我可以访问最多 30 天前的文件的先前版本。但是,考虑到每次我点击“保存”时都会存储一个新版本,检索每个版本,保存它,然后将其加载到我的 LaTeX 编辑器中进行编译似乎不太可行。更不用说当旧版本未保存在同一文件夹中时出现的相对路径问题了。

答案1

我不确定你是否想读这个...

只有第一的LaTeX 抛出的错误很重要,因为第一个错误经常会导致后续错误,而这些错误在你纠正第一个错误后就会消失。

所以如果你要查找错误

  • 仅需编写一小部分代码,无需编译!
  • 编译,
  • 仅查找第一个错误(通常位于新代码的一小部分中),
  • 找到它(好吧,这可能是一个艰巨的任务,但随着经验的增加你会做得更好)并且
  • 纠正它。忽略其他错误。现在
  • 再次编译(通常错误会少一个),
  • 只查找第一个错误,
  • ...

我习惯在每次思考下一段代码时进行编译。这样我只需要几行新代码,就能更快地找到错误。

如果您有大量代码需要更正,请在某些行后插入\end{document},然后编译并检查错误。如果一切正常,请\end{document}向下移动一些行,然后重新编译,依此类推。

正如@HaraldHanche-Olsen 在他的评论中所说:

“如果你怀疑错误可能出现在较大代码块的任何地方,那么执行二分查找通常很有用。使用此技巧,不要一次\end{document}移动几行,而是 尝试将其放置在产生错误的位置和不产生错误的位置之间的大致中间位置。\end{document}

\iffalse我经常使用...来注释掉一个块\fi。当然,使用这种技术时,您必须注意不要包含环境的一部分。”

如果您在文档中使用\input{...}或,\include{...}请检查注释中是否有错误。如果是,您可以使用宏\endinput来实现类似的效果\end{document}(请参阅@JuanA.Navarro 的评论)。

如果您需要对 TeX 文件使用版本控制,请使用真实系统来使用不同版本,如 svn(subversion)... 在此网站上搜索 svn。

答案2

  1. 许多编辑可以被说服自动跳转到第一个错误。
  2. 如果您不明白错误信息,请直接谷歌搜索;至少我是这么做的。
  3. 前言中的错误经常会导致文本中某处出现第一个错误。例如:定义标题的代码中的错误将在第一个分页符后显示,更改部分的代码中的错误通常不会在文本中出现第一个部分之前显示。因此,如果您偶然发现不相关的错误消息,请查看错误消息提供的文本行周围是否使用了特殊元素,可能是第一次使用。转到前言中的定义并在那里搜索。

答案3

这个答案主要是以 TeX 开发人员的身份撰写的。
根据我的经验,不同类型的错误需要非常不同的调试策略。
因此,并不是 OP 所问的;尽管如此,我还是尽量做到全面。

我想这和https://tex.stackexchange.com/a/516657/250119


调试的一般方法是

  • 找出您编写的代码中导致错误的最近位置。

  • 确定代码中存在什么错误。

    通常这些错误是由于你错误地使用命令


第 1 部分 stacktrace

您必须知道如何确定错误发生的位置;换句话说,获取“堆栈回溯”。

TeX 有一个用于此目的的机制:\errorcontextlines 有什么作用?

不幸的是,它有很多限制。请参阅我的回答。


不直接相关的注释。

我正在努力改善那里的情况

  • 对 TeX 引擎进行一些补丁来存储 token 的原始来源
    (这会更难,因为引擎级别和 TeX 编程级别中 token 的内部表示非常非常不同),
    或者
  • 需要在代码中添加额外的注释来标记明确的放松点(或可扩展的放松点),以及打印回溯的内容。

目前,虽然没有这样的工具,但我的方法(确定错误发生的位置)包括\errorcontextlines尽可能猜测错误发生的位置,然后在(我认为的)放松点添加调试打印语句,以最大限度地定位有问题的代码。

我所说的放松点是指\relax可以在该点添加任意数量的点,以便它们可以被执行(除了第一个之外不会扩展),
粗略地说,它们是语句分离点。

例如

⟨\relax point⟩
\int_compare:nNnT ⟨not \relax point⟩ {⟨not \relax point⟩ 12} < {34} {
    ⟨\relax point⟩
    \iow_term:x {⟨not \relax point⟩ 123}
    ⟨\relax point⟩
}
⟨\relax point⟩

我所说的“添加调试打印语句”的意思是简单地\pretty:n {reached here}在您认为是放松点的点处添加例如,然后查看打印了哪些行。


\pretty:n {AAA}
\int_compare:nNnT {12} < {34} {
    \pretty:n {BBB}
    \iow_term:x {123}
    \pretty:n {CCC}
}
\pretty:n {DDD}

如果错误如下

> AAA
> BBB
! some error message...

那么错误就会发生在BBB和之间CCC


如果在添加调试打印语句的过程中错误消失了,则可能是

  • 某些命令吞噬了后面的某些参数,它变成了调试打印语句,而不是后面的语句,或者
  • 某些命令会提前查找例如可选空格,并且在看到漂亮打印语句时它会终止而不扩展以下命令。(例如,记得~在接受数字的命令后添加一个明确的空格,例如,在 TeX raw 之后添加\catcode x=12一个等等。使用 expl3 的包装器可以避免这种情况,但对于设置或此类 TeX 寄存器之类的东西,或者为了性能优化,您必须坚持使用原始 TeX 分配。)\relax\numexpr\catcode

如果它们被替换后\relax错误仍然没有发生,那么您可能可以通过添加\relax它们来解决问题。

虽然你也想了解当时出了什么问题。
看看小心参见上述要点的代码。


找到两个连续的松弛点,确保您确定错误发生在它们之间,并且错误仍然未修复,请继续下一部分。


第 2 部分。查找错误


确保你至少读过一遍 TeXbook。

引擎有时会以违反直觉的方式运行,如果您不知道这一点并“假设”它的行为方式,您会感到惊讶。

例子:为什么 \if 之后不遵守 \unexpanded?为什么即使设置了其他/活动的 catcode,换行符仍然会消失?

作为比较,Python 也有一些意想不到的行为https://stackoverflow.com/questions/3431676/creating-functions-in-a-loop 尽管如此,它们还是比 TeX 少得多。


确实,您无法记住所有这些;或者有时在调试程序时“忘记”一些,在这种情况下,以下方法应该会有所帮助。


符号:在以下段落中,我使用\pretty:n {⟨token list⟩}\prettye:n {⟨token list⟩}\pretty:V \⟨tl var⟩\prettyshow:N ⟨control sequence⟩等作为通用命令来打印出标记列表的内容/变量的含义。
它们取自我的包;您prettytok可以选择使用其他任何内容,例如 \show\showthe\typeout\showtokens、等。(作为包作者,我建议使用我的包,有关更多详细信息,请参阅包文档。)\tl_show:n\msg_expandable_error:nn


它是强烈不推荐要进入命令执行内部,可以使用 unravel 包,也可以通过打印 X-step-expansion。

反而就假设它是一个黑盒子

原因:

  • 实施过程往往非常复杂,而且通常不是导致问题的直接原因
  • 更重要的是,他们倾向于没有错误并且您想调试您的代码,而不是他们的代码。

尽管如此,如果错误确实出在库中而不是你的代码中(请参阅 expl3 的一些示例错误https://github.com/latex3/latex3/issues?q=is%3Aissue),下面的说明足以或多或少地创建一个 MWE 来向库报告错误。

如果你还想直接在库中查找并修复该问题,那么

  • 不要通过修补命令来调试,这在某种程度上相当于通过查看其编译的二进制代码来调试包。
    相反...
  • 下载源 dtx 文件,然后修改添加调试命令等等。

实际上,在 docstrip 的常见情况下,修改 sty 文件并不是那么糟糕。只有注释被删除并@@替换为包名称。结构或多或少被保留了下来。

注意

  • 请记住确保实际使用的是新的 sty/dtx 文件,而不是系统中安装的旧文件。
    例如,通过临时添加\error到本地副本,编译 TeX 文件,然后确保发生错误。

而且,这种情况很少见,但是...

不要使用调试工具来调试其自身。

因为,如果您修改基本工具,调试打印机可能会出现意外的行为,这并不奇怪。

对于原始对象来说\immediate\write16没有问题,但除此之外要小心。


打印出将要执行的代码。

例如

\cs_new_protected:Npn \function #1 {
    \pretty:n {\tl_something:Nnn \tlvar {#1} {...}}  % ← add this.
    \tl_something:Nnn \tlvar {#1} {...}
}

这看起来可能多余,但实际上可能会有帮助:

  • 有时 TeX 标记化规则可能与你预期的不同。例如
    • ^^⟨content⟩将成为不同的代币
    • 如果您输入\pretty:n { \abc ~ },则\abc只会打印出,而不会显示明确的空格。
  • 此外,请记住 TeX 编码不仅仅是“用这个变量值运行这段代码”,它是自修改代码

如果命令是“顶级”的,它往往不会产生任何输出,请将\pretty:V \resultvariable下一个松弛点插入到存储结果的位置。否则,您可以例如\pretty:x {\tl_something:nnn {...} {...} {...}} 查看结果。


一旦您确定了命令中传递的具体内容(如果没有出错,还可以选择确定命令产生的结果),请仔细阅读该命令的文档,看看在给定该参数的情况下它是否应该这样做。

很有可能,你准备的参数不正确,那么错误就出在准备参数的代码中。


您可能还想尝试 在单独的独立文档 (**)中使用该特定参数(*)调用该特定命令 ,看看它是否以相同的方式出错。

如果没有,问题可能出在某些“全局共享状态”(例如 escapechar 的值、某些特定宏的定义等),因此请调查此事。

不幸的是,TeX 中存在许多“全局状态”,尽管 expl3 命令往往不会出现这个问题。

(*): 如果您由于其具有非标准 catcode 而难以提供参数,我有precattl

(**):实际上,如果参数依赖于定义的其他命令,则这相当困难。但这就是想法。另外然后您可能希望打印出将要传递给参数的参数的值,以查看是否输入正确


有时文档不清楚(参考在哪里可以找到命令/环境是如何定义的?在哪里可以找到文档,但是某些命令的文档并不那么清楚。

尝试使用该命令(通过进行 MWE)来确保您正确理解它。

最值得注意的是 e-TeX 命令,它etex_man.pdf非常简短。如果您遇到涉及以下命令的意外行为,请确保您完全理解链接问题中的答案所解释的内容。

有时 expl3 命令文档不是很清楚,例如,\tl_replace_all:Nnn仅在顶层运行。)


谈论\scantokens,您应该能够实现一个不可扩展的\debugscantokens命令,该命令执行与类似的操作 \scantokens,但将内容写入临时文件,这样您就可以更轻松地检查临时文件的内容以查看它是否正确。

尽管这并不一定以相同的方式表现,例如即使 newlinechar 不是 10,charcode-10 标记也会仍然在生成的文件中创建物理换行符,但仅限于物理文件。无论如何,您都不应该依赖此行为。


对于调试可扩展命令或调试输入流操作命令,我的包分别有\prettye:n\pretty[e]:w,尽管您可能选择自己重新实现类似的东西。

\pretty:w但是对于 catcodes 来说是破坏性的,所以这是一个限制。(有点像“只能使用一次调试打印机”)


调试可扩展命令的另一种方法包括打印命令的 X-step-expansion。

一种方法是使用\exp_args:No \exp_args:NNo \pretty:o { ... }上面提到的方法(请注意,这与多步扩展并不完全相同,因为 o 型扩展会将未扩展的标记转换为普通标记;并且如果生成了一些不平衡的括号,就会有麻烦。

有一个multiexpand包可以简化这种写法。因此\pretty:o {\romannumeral \multiexpand {5} ...},或\expandafter \prettye:w \romannumeral \multiexpand {5} ... \prettystop对于不平衡括号的情况。


对于 expl3 代码,如果参数规范不是基本形式,则打印出实际传递给命令的输入会有所帮助。

除非代码使用 l3flag 的机制来保持状态(即\csname ... \endcsname定义控制序列为\relax之前未定义的状态),否则它将是幂等的。因此,如果你有

\exp_args:NV \tl_something:oxV \a {\b \c} \d

那么你可以写

\exp_args:NV \pretty:oxV \a {\b \c} \d

确定什么是实际上将被传递给\tl_something函数。

与上面提到的“打印X步扩展”方法相比,该方法具有以下优势

  • 你不需要确切地知道\exp_args:N*完成了多少个扩展步骤
  • 有些\exp_args是无法扩展的(x 型扩展等),所以你需要类似\unravelpackage; 的东西,但是目前,需要一步一步地/与控制台交互(公平地说,unravel 有u⟨text⟩快进到裸命令的命令,但这可能会因拼写错误等而超出范围)

如果问题出在“全局状态”,那么合乎逻辑的下一步就是在执行命令之前打印出全局状态的值

您如何知道哪些全局状态是有趣的?最有可能的是您在代码中修改的那些状态。(您可能会忘记将值恢复为正常值)


一个例子“全局状态修改”错误。(我的问题)

为什么从第二次编译开始我会得到 `! LaTeX Error: Missing \begin{document}. l.2 gdef @abspage@last{1}`?

这个,我通过...调试的。

  • 首先,找出错误内容发生在辅助文件中(例如--file-line-error

  • 意识到通常,要写入辅助文件的字符串具有以下形式\gdef \@abspage@last {1}。(阅读 LaTeX 用户手册对这一点没有帮助。您可以阅读 source2e,或者更简单,尝试编译一个简单的工作文档)

  • 与此相比,\ 消失了。

分析:

为什么它如此令人沮丧并且需要了解 (La)TeX 的这么多内部实现细节?

在其他语言(例如 Python/Lua)中,如果修改了一些常用的内置全局变量(例如list或)tonumber,那么在稍后不确定行数的某些库中出现一些神秘的错误也就不足为奇了。

(在这方面 Lua 比 Python 差,因为它的全局命名空间是真正的全局的)

但是,在这里,我们没有工具/原语来例如“获取带有 escapechar=X 的标记列表的字符串表示”,而是必须修改全局变量\escapechar来达到结果。

或者——由于 escapechar 已被修改,所以我认为,可以简单地在文档末尾打印出 escapechar 的值并看到它是 -1。


另一种情况。我意外地设置了\let \FCDtabtomacro \⟨some typo that results in an undefined macro⟩,然后调用filecontentsdefmacro环境并得到“未定义的控制序列”和无用的回溯。

错误上下文在这里不是很有用,但是你可以

  • 尝试打印出“全局状态”(在这种情况下它应该包括的定义\FCDtabtomacro),因为您在调用之前对其进行了修改filecontentsdefmacro
  • 读取日志文件而不是终端,并找到某种方式来明显显示制表符,例如:set list在 vim 中(相当困难,除非您确切知道错误是什么......)

结论:您可以采用与调试其他编程语言相同的方式对其进行调试。

评论:作为一个知道如何使用 gdb 调试 C++,但不知道如何使用 pdb 调试 Python 的人,我想说快速编译(或交互式 shell)和调试打印对于真正的调试器来说是一个不错的替代品。

例如,您不需要在调试器中打印出值,而是可以\pretty:nV {variable=} \⟨the variable⟩ 在出错的行之前添加一行,然后读取输出中打印出的最后一个值。

需要快速编译/执行,因为否则您将需要在每个调试步骤之前等待很长时间,这在每种语言中都是令人沮丧的。(因此,正如我上面提到的,如果您没有快速编译,则需要交互式 shell 或调试器)

我并不是说调试器是无用的;事实上,我已经写了一个调试器来做一些其他方式无法完成的事情(目前调试器可以

  • 检查“输入流”的内容,区分标记化/未标记化的内容,但不触及内容本身——这不是通过unravel包完成的,据我所知,没有修改编译器的任何现有方法,并且
  • 执行一步(标准功能)

但是它需要大约 10 倍以上的功能才能像漂亮的打印调试一样有用。)

而且我目前对使用漂亮的打印调试非常满意,所以这种情况不会很快发生。


其他说明


一些软件包(例如 expl3)有调试模式,请尝试阅读文档并启用它(如果有)

然而对我来说,它似乎不是很有用。


根据配置,反复停止/启动编译器和/或在日志文件/详细的终端输出中搜索错误消息可能会很繁琐。

參閱编译 - 减少 LaTeX 的控制台输出寻找使该过程更加简单的方法。

避免在 LaTeX 控制台/日志输出中出现换行符(或在终端中增加列)可能对查看错误上下文有点帮助,但前提是查看错误上下文的地方不换行(对我来说,在日志文件中读取比在终端中读取更好,因为没有换行并且保留了尾随空格 - 尽管如此,请确保您阅读了所有行,因为有些行可能会缩进很多)


有时,您的包命令会产生正在排版的虚假字母。

通常,这是由以下原因造成的......

调试这个相当困难,但我发现一个相当好的方法是将整个代码放在序言中,所以 LaTeX 有必要的机制(everypar hook)在排版时给出错误,这样你就可以快速找到错误的位置。

如果一切都失败了,您仍然可以进行二进制调试。

与具有多行代码的 Python 程序相比,每一行都可能会或可能不会将全局变量的值设置为不需要的值,这将很难追踪。(使用调试器,您可以在变量上设置内存观察点,但 TeX 中目前没有调试器)


unravel包裹

一步步仿真TeX 引擎。

如上所述,它不支持\directlua其他 XeTeX/LuaTeX 特定的东西(新的字体处理等)和 catcode 更改命令。

参考:宏 - LaTeX 日志分析器应用程序(可视化 TeX 扩展) - TeX - LaTeX Stack Exchange

(尽管标题如此,但那里的答案宣布了包unravel。TeX 日志文件太难解析,因为所有的 catcode 信息都被删除了——此外,如果\escapechar=-1,那么输出中的反斜杠也会被完全删除)

我个人认为这不比 printf 调试有用然而


除了 TeX 宏之外\tracingall,还有一个声称trace可以使输出更漂亮的包。

也可以看看:调试 - Latex \tracing 命令列表? - TeX - LaTeX Stack Exchange

就我个人而言,我没有太多使用跟踪进行调试的经验,除了在某些特殊情况下,例如我的答案这解释了如何找到失控参数开始的行号。


这是从用户的角度来看的。

没有错误,但输出很奇怪

如果存在实际错误,则这个错误比那个更难追踪,因为很难确定代码中错误发生的位置。

首先,阅读文档小心确保软件包支持您想要的内容。在几乎所有情况下,该行为已记录在手册中. (否则这将是一个错误。)

我怎么知道应该阅读手册中的哪里?

  • 举一个最简单的例子。例如,如果代码当前使用组件 A、B 和 C,并且如果您用虚拟文本替换 B 和 C,问题仍然存在,那么您只需要(重新)阅读组件 A 的文档。

举个例子。

如何平衡同一页面中的两个框架框?

正如您所知……在这种情况下,该行为已记录在文档中,但通读整个 LaTeX 手册来查找该行为并不是很实用。


另一个例子。

上次我\int_step_inline在表格环境中使用,它创建了一个虚假单元格(请参阅表格内容的管道输入工作异常作为示例。

这是一个很难调试的问题(我最终在 TeX.SE 聊天中询问),但我认为可以通过以下方式调试...

  • 首先,创建一个 MWE。应该不难,最后你只需要一个tabular环境和一个\int_step_inline

  • 用 TeX 原语替换 LaTeX 命令。

特别是,tabular带有环境halign,以及int_step_inline一些你自己实现的循环(通常这比尝试调试 expl3 实现更容易,即使 expl3 有完整的文档记录)

如果当你替换某个东西时错误消失了,那么你就知道刚刚替换的东西要么有错误。

无论如何它不应该以那种方式使用,所以一个修复方法是做一些保证工作的事情(在这种情况下,它将在int_step_inline运行表格环境之前构建完整的令牌列表)

否则,这是因为 LaTeX 工具从 TeX 原语继承了一些怪癖——
这里就是这种情况,它halign完全扩展第一个参数,直到看到不可扩展的东西,如果不是 egroup,那么它就会启动一个新的单元。


一个包装错误的情况。

列表标题中的方括号

(据我所知这是一个软件包错误,因为[]手册中没有写出这种不寻常的行为)

在这种情况下,您应该能够快速找出错误所在,并进行 MWE,而无需深入研究包的实现细节。

在实践中,你想知道如何解决问题以及。(无需联系软件包作者并等待回复)

要想出一个解决办法并不容易,但是如果你对 LaTeX 的实现方式有所了解,就会发现它[被意外地当作可选参数的分隔符而不是要排版的某些文本,你可以在前面加上\relax它来禁用它。


有时你的程序是正确的,但是运行速度很慢。这是另一个话题。


评论:

我的观点是,调试 TeX 比调试 Python/C++ 更难,但差别并不大;
并且,使用更好的调试工具,相同的编程任务将花费相同的精力进行调试;
尽管如此,在 TeX 中执行的任务往往比在 Python 中更复杂(在典型的编程语言中进行排版并不常见)。

\textsf对于OP在/上的示例\enquote,我的观点是,如果Python具有类似的功能,则错误也会同样具有误导性
(因为计算机首先收集所有文本及其使用的字体;然后对其应用微类型调整。错误发生在文本被第一次收集之后,因此行号具有误导性。) -
除非作者故意添加代码来记录收集代码的堆栈跟踪。

相关内容