> For the complete documentation index, see [llms.txt](https://ayakaleaf-pro.ayaka.space/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ayakaleaf-pro.ayaka.space/latex/zh-cn/shen-du-wen-zhang/09-an-introduction-to-luatex-part-2-understanding-directlua.md).

# LuaTeX 简介（第 2 部分）：理解 \directlua

## 本文的目标

在本文的第一部分， [LuaTeX 简介（第1部分）：它是什么——又是什么让它如此与众不同？](/latex/zh-cn/shen-du-wen-zhang/07-an-introduction-to-luatex-part-1-what-is-it-and-what-makes-it-so-different.md)，我们简要回顾了 LuaTeX 作为一个极其灵活的 TeX 引擎：一种复杂、可编程的排版系统，提供了用于构建设计文档与生产方案的广泛工具。

在这篇收官之作中，我们将仔细研究 LuaTeX 工具箱中最重要的组成部分： `\directlua` 这个命令通过 Lua 脚本语言提供了对 LuaTeX 排版进行程序化控制的“入口”。

不过，要通过 `\directlua` 则需要一些 TeX 主题方面的背景知识：TeX 的记号、记号列表和展开机制。本文的目标是探索并解释这些基础 TeX 概念：拼接出与 TeX 相关、位于……背后的各个处理过程 `\directlua` 以便理解它的工作方式，并为你使用 LuaTeX 构建自己的排版方案奠定基础。

本文包含大量简短示例，用来演示并解释……的关键方面 `\directlua`的行为，刻意避免过于复杂的代码，而偏向于简短的代码片段。必要时，示例会使用基础（原始/纯）TeX——尽管大多数人使用并偏好 LaTeX（宏），基础 TeX 命令的优点在于简单。

## LuaTeX 中的 Lua 简介

[Lua](https://www.lua.org/about.html) 是一种脚本语言，其 [源代码](https://www.lua.org/download.html) 具有很强的可移植性，也很容易嵌入到软件应用中，使开发者能够把脚本能力集成到他们的程序里。Lua 已被嵌入到 [许多应用程序中](https://en.wikipedia.org/wiki/List_of_applications_using_Lua) ，并且在软件游戏行业中是一个很受欢迎的选择——也许最著名的例子是 [《魔兽世界》](https://wowwiki.fandom.com/wiki/Lua_functions).

LuaTeX，顾名思义，是一个嵌入 Lua 脚本语言的 TeX 引擎，使用户能够通过在文档中加入 Lua 程序（脚本）来控制 LuaTeX 的排版行为。除了直接控制 LuaTeX 之外，用户还可以把 Lua 单纯当作一种非常强大的编程语言来执行那些用 TeX 语言可能极难实现的任务——而 TeX 语言无论如何衡量，都是一门难学也难精通的语言。通过添加并集成 Lua，LuaTeX 变成了一个非常灵活而强大的 TeX 引擎，直接支持两种编程语言。

### 在文档中使用 Lua 和 TeX：输入 \directlua

Lua 和 TeX 是两种 *截然不同的* 编程语言：Lua 比大多数人所理解的编程语言更接近其定义，但 TeX 由于其类别码、记号、宏和展开机制，和大多数人对于“编程语言”的经验/预期相去甚远。然而，历史已经表明，TeX 语言之所以能够经久不衰，是因为它在自己被设计出来要做的事情上非常出色：控制排版，即使它的运行方式多少有些深奥。

为了解决在同一个 TeX 文档中混合 Lua 和 TeX 语言这一挑战，LuaTeX 的开发者引入了一个新命令，名为 `\directlua` 它是使用 Lua 的途径——既可将 Lua 作为独立的编程语言使用，也可用来控制 LuaTeX 的排版行为。

该 `\directlua` 命令允许用户在 TeX 文档中嵌入 Lua 代码；这些代码随后会传递给 LuaTeX 内置的 Lua 语言解释器。不过， `\directlua` 也允许你 *将* Lua 和（La）TeX 代码在同一个 `\directlua` 命令中组合在一起——尽管由于 Lua 和基于 TeX 的编程语言之间存在根本差异，这会带来额外的复杂性。使用（La）TeX 与 Lua 代码的组合时，关键挑战是确保这两种语言和谐共存，而不会“互相掣肘”。

`\directlua` 最适合用于文档中较短的 Lua 代码片段，但如果你愿意，也可以将它用于更长的 Lua 程序。通常，更庞大的 Lua 程序和 Lua 代码库会保存到外部文件中，并可通过 Lua 的 `dofile()` 函数，在一个 `\directlua` 命令中调用。从 TeX 处理的角度来看，使用外部 Lua 代码文件的一大显著优势，是可以避免因 TeX 的类别码机制而产生的复杂性——本文将对此作全面探讨。

### \directlua 的更正式描述

该 [LuaTeX 参考手册](http://www.pragma-ade.com/general/manuals/luatex.pdf) 将其描述为 `\directlua` 如下（略有改动）：

> 为了将 Lua 代码与 TeX 输入合并，需要一些新的原语。原语 `\directlua` 用于立即执行 Lua 代码。基本语法是 `\directlua{⟨code⟩}`。 `⟨code⟩` 会被完全展开，然后送入 Lua 解释器。经过读取和展开应用到 `⟨code⟩`， `\the\toks`.

当然，这在技术上是准确的，但如果不了解较底层的 TeX 过程——例如记号和展开——也许并不容易理解。

## 理解 \directlua：我们将涵盖哪些主题？

在本文中，我们将更仔细地查看一些关键背景主题，并提供若干示例，旨在演示 `\directlua` 的工作方式，以及在你的……中将 TeX 与 Lua 结合时何处（或为何）需要小心 `⟨code⟩`.

我们将以足够详细的方式探讨以下主题，为理解 `\directlua` 及其对你在其中使用的代码进行的“预处理”奠定基础：

* 类别码和 TeX 记号：将文本转换为记号，以及将记号转换为文本；
* TeX 的展开过程（以及如何防止展开）；
* Lua 中用于字符和字符串的转义序列/机制；
* 使用 Lua 风格的注释；
* LuaTeX 的 Lua API 简短介绍。

如果你理解 TeX 引擎如何创建和使用记号，并对 TeX 的展开机制有所认识，那么你就具备了解锁 LuaTeX 的惊人多功能性所需的基础 `\directlua` 该命令。

## 基础：从文本到记号，以及从记号到文本

Overleaf 已经发布了几篇深入探讨 TeX 记号及相关概念的文章，因此这里我们不再重复这些内容；相反，我们会概述那些有助于更好理解……的相关领域/主题 `\directlua`.

以下是一些可能感兴趣的已发表文章：

* [什么是 TeX 记号？](/latex/zh-cn/shen-du-wen-zhang/53-what-is-a-tex-token.md)
* [什么是 TeX 记号列表？](/latex/zh-cn/shen-du-wen-zhang/54-what-is-a-tex-token-list.md)
* [\expandafter 是如何工作的：TeX 记号简介](/latex/zh-cn/shen-du-wen-zhang/19-how-does-expandafter-work-an-introduction-to-tex-tokens.md)
* [六部分系列：TeX 宏究竟是如何工作的？](/latex/zh-cn/geng-duo-zhu-ti/01-a-six-part-series-how-do-tex-macros-actually-work.md)

### 理解字符记号

TeX 引擎能够从文本文件中读取的任何字符都由两个数值表示：

* 它的 *字符码* （ASCII 值，或如今的 Unicode 码点）；
* 另一个以 TeX 为中心的数值，称为其 *类别码*.

想要进一步了解类别码的读者，可能会对 Overleaf 发布的这篇介绍感兴趣： [那么我们从哪里开始？从类别码开始](/latex/zh-cn/geng-duo-zhu-ti/19-how-tex-macros-actually-work-part-1.md#so2c-where-do-we-start3f-with-category-codes).

例如，如果某个 TeX 引擎读入一个字符 `一个` 它就能获得两条信息： `一个`的字符码（65）以及它的类别码（通常为 11）。一旦 TeX 输入了该字符 `一个`，它的类别码不会改变，但用户宏可以修改类别码，这可能会影响任何 *后续* @ `一个` 尚未被 *TeX 读取的* 内容。因此，TeX 需要记录 *这个* @ `一个`, *刚刚读入的*，其类别码为 11。为此，TeX 使用整数对 (65,11) 计算出另一个整数值，称为 *字符记号*。通过计算该记号值，并将其传递给 TeX 的内部处理，该特定 `一个` 以及它的类别码就被 *绑定在一起*；实际上，这个字符记号 *封装了* TeX 需要了解该字符的数据，以便在 TeX 引擎更深层内部的后续任何排版活动中使用。

#### 字符记号是如何计算的？

首先，我们需要记住，TeX 引擎会使用类别码 13 来创建所谓的 *活动字符*：任何类别码为 13 的字符都表现得像一个迷你宏；因此，正如下文所示，活动字符的记号计算方式与其他类别码（如 10、11 或 12）的普通字符不同。

对于 *非活动* 字符：

* 较旧的 8 位引擎（Knuth 的 TeX、e-TeX、pdfTeX）会为 *非活动* 字符计算记号时使用

$$\text{(non-active) character token} = (256 \times \text{category code}) + (\text{ASCII character code})$$

* 而对于 LuaTeX 来说，由于它必须处理 Unicode 字符值， *非活动* 字符的计算方式相似，但会产生大得多的整数值：

$$\text{(non-active) character token} = (2^{21} \times \text{category code}) + (\text{Unicode value})$$

回到前面那个类别码为 11 的字母 A 的例子，LuaTeX 会计算出一个字符记号值 $$2^{21} \times 11 + 65 = 23068737$$。一旦计算出来，该字符记号值 *将* 特定的字符 A 与类别码值 11 绑定在一起。用户宏可能会改变后续任何字符 A 的类别码，但这个字符的类别码已经通过将其转换为记号而固定下来，以便在穿过 LuaTeX 的内部运作时使用。LuaTeX 已保存，或者说封装了，该字符在读入时所确定的原意。

TeX 引擎总共使用 [16 种不同的类别码](/latex/zh-cn/geng-duo-zhu-ti/43-table-of-tex-category-codes.md) 和 *任何* 这些类别码都可以通过 `\catcode` 命令赋给 *任何* TeX 引擎能够读取的字符。对类别码的更改用于改变 TeX 引擎处理输入中特定字符的方式，使 TeX 用户能够编写产生特殊排版结果或行为的宏。

**活动字符**

如前所述，TeX 引擎使用类别码 13 给字符赋予“特殊含义”，使其成为所谓的 *活动字符* ，其行为类似一个迷你宏：不需要前导 `\` ，这个独立字符由于其类别码就足以触发类似宏的行为。

因为活动字符像迷你宏一样工作，所以它不会被转换为 *字符记号* ，而是转换为另一种（整数）记号类型，称为 *命令记号*。其计算方式如下：

* 对于较旧的 8 位引擎（Knuth 的 TeX、e-TeX、pdfTeX），活动字符的记号通过以下方式计算：

1. 计算一个称为 $$\text{curcs}$$ (**cur**rent **c**ontrol **s**equence）其中 $$\text{curcs} = \text{character code} + 1$$3. 计算记号值，其中 $$\text{active character token} = \text{curcs} + \text{4095}$$

* 对于 LuaTeX 来说，计算稍微复杂一些，因为它必须处理完整范围的 Unicode 字符，而其中任何一个都可能被设为活动字符：

1. 计算中间整数值 $$\text{curcs}$$ 通过应用一种所谓的 *哈希函数* 于以 UTF-8 表示的该活动字符 Unicode 码点值： $$\text{curcs}=\texttt{hashfunction}\text{(UTF-8 text for Unicode value of active character)}$$3. 计算整数记号值： $$\text{active character token} = \text{curcs} + 2^{29} - 1$$

**示例**

* 8 位引擎：活动字符 `~` （字符码 126）的记号计算结果为 $$\text{curcs} = 126 + 1 = 127$$，得到的记号值为 $$4095 + 127 = 4222$$.
* LuaTeX：活动字符 `~` 的记号计算结果为 $$\text{curcs}=3186$$ 得到的记号值为 $$3186 + 2^{29} - 1 = 536874097$$。LuaTeX 的记号使用大得多的整数值！

### 理解命令记号

除了处理 *单个* 字符之外，TeX 引擎当然也可以处理 *序列* ，也就是称为 *命令* （或者更准确地说， *控制序列*）。按照传统， `\` 字符用于标示一个命令的开始，但这仅仅是一种约定——实际上，任何类别码为 0（转义字符）的字符都可以用来代替。

TeX 引擎识别两种命令，分别称为 *控制字* 和 *控制符号*:

* **控制字**：由一个或多个类别码为 11 的字符构成的命令；
* **控制符号**：单字符命令，其中该字符的类别码 *并未* 为 11：例如 `\$`, `\#` 或 `\\`.

**注意**：TeX 原语 `\chardef`, `\mathchardef`, `\countdef`, `\dimendef`, `\skipdef`, `\muskipdef` 和 `\toksdef` 也用于定义控制序列，但与常规宏定义不同，得到的控制序列（控制字或控制符号） *不可展开*——我们将在下面更详细地探讨它们。

#### 命令记号是如何计算的？

和活动字符一样，TeX 引擎使用第二种整数记号值来表示命令： *命令记号*——请记住，活动字符也会生成命令记号，因为它们表现得像迷你宏。

8 位引擎用于创建命令记号整数的计算方法可在这篇 [Overleaf 文章](/latex/zh-cn/shen-du-wen-zhang/19-how-does-expandafter-work-an-introduction-to-tex-tokens.md#how-tex-calculates-token-values)中找到。这里，我们将总结 LuaTeX 中命令记号计算的关键步骤——由于 LuaTeX 必须处理 Unicode 字符码值，而它们可能远大于 8 位值，因此这些步骤略有不同；不过，LuaTeX 的计算遵循与较旧 8 位引擎相同的一般原则。

在检测到传入命令后，包括 LuaTeX 在内的 TeX 引擎会忽略前导 `\` 字符：它不用于命令记号值的计算，而只是充当一个“开关”，告诉 TeX 引擎需要处理一个命令。命令记号值是根据命令名称中出现的（一个或多个）字符序列计算的——LuaTeX 使用同一算法为控制符号和控制字计算命令记号：

1. 计算中间整数值 $$\text{curcs}$$ 通过应用一种所谓的 [哈希函数](https://en.wikipedia.org/wiki/Hash_function) 于命令名称中包含的 Unicode UTF-8 字符串： $$\text{curcs}=\texttt{hashfunction}\text{(Unicode UTF-8 string of characters in command name)}$$3. 计算命令记号值，其中 $$\text{command token} = \text{curcs} + 2^{29} - 1$$

**示例**

* 用于 `\\` 命令（一个控制符号），LuaTeX 计算 $$\text{curcs}=94$$，从而得到 `\\` 的记号值 $$94 + 2^{29} - 1 = 536871005$$.
* 用于 `\vskip` 原语命令（一个控制字）时，LuaTeX 计算 $$\text{curcs}=3560$$，从而得到 `\vskip` 的记号值 $$3560 + 2^{29} -1 = 536874471$$.
* 针对用户定义宏 `\mynewmacro` （一个控制字）时，LuaTeX 计算 $$\text{curcs} = 2971$$，从而得到 `\mynewmacro` 的记号值 $$2971 + 2^{29} -1 = 536873882$$.

一旦创建，记号就可以通过所谓的 *记号列表* ，也可以立即传递到 TeX 引擎内部继续处理。使用整数值来表示记号不仅适用于各种计算平台/操作系统，而且也是 TeX 存储/处理数据的一种非常高效的方式。

### TeX 引擎如何识别记号类型（命令或字符）

给定某个特定的整数记号值， $$T$$TeX 引擎就可以轻松判断 $$T$$ 它表示的是命令还是字符，只需测试它是否 $$T$$ 超过某个 $$\text{threshold value}$$——该阈值 $$\text{threshold value}$$ 取决于 TeX 引擎。如果 $$T \geq \text{threshold value}$$ 那么 $$T$$ 是命令记号，否则是 $$T$$ 字符记号。 $$\text{threshold value}$$ 是 $$4095$$ 对于 8 位引擎为 $$2^{29}-1$$ （536,870,911）对于 LuaTeX。Knuth 设计了用于记号计算公式的方法，使他的 TeX 引擎以及所有基于其代码/架构的后续引擎都能快速而轻松地测试记号值。

## 记号可以被拆解（并转换回文本）

记号（整数）是 TeX 引擎“封装”其需要记录的所有输入项信息（字符或命令）的机制。不过，有时 TeX 引擎需要反转记号化过程——弄清楚最初读入了什么才生成了该记号值——是单个字符，还是构成某个命令名称的一个或多个字符序列：

* **对于字符记号**：任何字符记号都可以拆分为两个组成部分：该字符的字符码，以及分配给该字符的相应类别码 *，即它最初被读入时*。与所有 TeX 引擎一样，LuaTeX 不会改变这一原始类别码分配，但会在后续内部处理活动中使用它。
* **对于命令记号：** 这些稍微更详细一些，但如果你查看 LuaTeX 对命令记号的计算，包括活动字符的记号，你会发现它们遵循一个模式： $$\text{command token} = \text{curcs} + 2^{29} -1$$

其中 $$\text{curcs}$$ 会根据所生成的命令记号类型来计算：活动字符、控制符号或控制字。 $$\text{curcs}$$ 变量是一个 *极其* TeX 引擎内部运作的重要组成部分：给定任何命令记号（整数）值，LuaTeX 都可以非常容易地提取 $$\text{curcs}$$ 从该命令记号中的值，使用 $$\text{curcs} = \text{token value} - (2^{29} -1)$$.

### 为什么 $$\text{curcs}$$ 如此重要？

TeX 内部变量 $$\text{curcs}$$ (**cur**rent **c**ontrol **s**equence）是 TeX 引擎内部“幕后”运作中至关重要的组成部分。尽管你不会、也不能在代码中直接使用或访问它， $$\text{curcs}$$ 但它起着关键作用，因为 TeX 引擎会使用 $$\text{curcs}$$ 的当前值作为内部表的索引，这些表存储着引擎当前已知的每个命令的数据。这些表存储命令当前含义的信息：它做什么，或者表示什么；此外，它们还记录最初用于计算该 $$\text{curcs}$$ 值时所用的字符序列。通过提取 $$\text{curcs}$$ 从命令记号中的值，TeX 引擎就能够确定与任何（命令）记号对应的名称，也就是人类可读文本，从而能够执行记号到文本的转换，而这正是 `\directlua`运行的关键方面。

### 将整数记号转换回字符或字符序列（命令名称）

我们已经看到，TeX 引擎会将输入字符或字符序列转换为整数记号值，但有时 TeX 引擎需要 *反向执行* 这一过程——输出最初用于创建这些整数记号值的人类可读文本；例如：

* 把错误或警告信息写到屏幕上，或者 `.log` 文件；
* 通过 `\write` 命令更改；
* 在将记号序列转换为文本时，使用 `\directlua` （我们很快就会看到！）

#### 将字符记号转换为文本

如前所述，非活动字符的记号使用输入字符的类别码和字符码（Unicode 值）来计算。LuaTeX 使用如下公式：

$$\text{character token} = (2^{21} \times \text{category code}) + (\text{Unicode value})$$

将整数 $$\text{character token}$$ 值拆分以获得其组成的字符码（$$\text{Unicode value}$$）以及 $$\text{category code}$$.

#### 将命令记号转换为文本

所有 TeX 引擎都会存储它们“知道”的每个命令的名称（字符序列）：无论该命令是用户定义宏还是内置原语——原语命令名称的存储发生在 TeX 引擎启动时，远早于它开始处理你的代码。对于用户定义命令（宏），该宏的名称（去掉前导 `\`）会作为 TeX 引擎内部宏定义过程的一部分被存储起来。

当 TeX 引擎需要访问或输出最初用于计算某个整数命令记号的人类可读文本时，它会先确定该记号的 $$\text{curcs}$$ 值；在 LuaTeX 中， $$\text{curcs} = \text{token} - (2^{29} -1$$）。利用 $$\text{curcs}$$ 从命令记号中提取出的值，TeX 引擎可以访问一个称为 *字符串池* ，以确定最初用于计算该特定值的、人类可读的字符序列，用于 $$\text{curcs}$$ ，从而得到相应的命令记号。

正如我们将看到的，这些记号处理活动——将字符序列转换为整数记号值，以及将整数记号值转换回字符序列（“反记号化”）——是 *基本机制* 在 `\directlua`.

## 记号列表

当 TeX 引擎读取输入、生成字符记号和命令记号（并对其进行处理）时，它可能会遇到某些命令，这些命令指示引擎（临时）停止将记号继续传递以供进一步处理，而是将它们存储起来以备后用。最常见的例子是使用某个宏定义命令来定义宏 `\def`, `\edef`, `\gdef` 或 `\xdef`——像 LaTeX 命令 `\newcommand` 就是围绕底层原语构建的宏，它们最终执行实际的宏定义过程。一个宏可以看作赋予某个特定存储记号列表的名称：一个记号列表。

TeX 引擎会大量 *广泛地* 使用记号列表，尤其是 [仅供内部使用的临时列表](/latex/zh-cn/shen-du-wen-zhang/21-how-does-expandafter-work-tex-uses-temporary-token-lists.md) ，用于内部处理目的。每个 TeX 引擎也都提供用户级命令来创建记号列表，以便在用户或 TeX 引擎本身需要时调用。创建记号列表的命令（内置原语）数量会因 TeX 引擎而异，但它们都共享每个引擎支持的一组核心最小命令，例如 `\toks` 原语。

实际上，记号列表不过是一个存储的整数值序列：

* 读取输入以生成（计算）单个记号，表示一个字符或命令；
* 然后每个记号被存储起来，保留它们从输入中生成时的顺序。

TeX 引擎使用一种称为 [链表](https://en.wikipedia.org/wiki/Linked_list) （单向链接类型）的数据结构来存储记号列表。想进一步了解记号列表的读者可以阅读 Overleaf 文章 [什么是 TeX 记号列表？](/latex/zh-cn/shen-du-wen-zhang/54-what-is-a-tex-token-list.md) ，它使用类比来构建记号列表背后的概念/思想。关于 TeX 记号列表以及它们在宏处理中的使用方式的深入探讨，可参见 Overleaf 系列文章 [TeX 宏究竟是如何工作的？](/latex/zh-cn/geng-duo-zhu-ti/01-a-six-part-series-how-do-tex-macros-actually-work.md)

#### 图示形式的记号列表

下图展示了一个由 LuaTeX 生成的记号列表，以及由以下输入产生的相应记号值

`Hi, \TeX! \hskip 5bp`

例如，如果我们定义 `\mymacro` as `\def\mymacro{Hi, \TeX! \hskip 5bp}` ，那么 `\mymacro` 的定义会使用如下这样的记号列表存储在内存中：

![](/files/0f9c40b104a47f411e3dc22a0cf4106a7d4c6721)

记号列表是一系列相互链接的项目，称为 *节点*，这是分配给 LuaTeX 内存中用于保存列表中每一项的小块内存的名称（就像链中的单个链接）。每个节点包含一个整数记号值，以及 *下一个* 链中节点的内存地址，形成一种称为 [链表](https://en.wikipedia.org/wiki/Linked_list)的数据结构。最后一个节点使用一个特殊的“空值”表示下一个节点，从而标示列表结束——因为后面已经没有节点了。

**注：**

* 为了方便，我们包含了每个单独节点的地址，但实际上，这些数据并不会存储在记号列表节点中；只需要 *下一个节点* 的地址来构建 TeX 引擎记号列表。
* 标题为“每个记号的含义”的图中第二列显示了一系列灰色框，包含每个节点中所含记号的信息：这些纯属说明， *不会* 并不构成存储在记号列表中的实际数据的一部分。

以下是上图所示记号列表中包含的记号值表：

|         |          |                                                             |           |
| ------- | -------- | ----------------------------------------------------------- | --------- |
| **输入项** | **输入类型** | <p><strong>类别码</strong><br><br><strong>（如果是字符）</strong></p> | **记号值**   |
| H       | @        | 11                                                          | 23068744  |
| i       | @        | 11                                                          | 23068777  |
| ,       | @        | 12                                                          | 25165868  |
|         | @        | 10                                                          | 20971552  |
| \TeX    | 命令（宏）    |                                                             | 536871539 |
| !       | @        | 12                                                          | 25165857  |
|         | @        | 10                                                          | 20971552  |
| \hskip  | 命令（原语）   |                                                             | 536874247 |
| 5       | @        | 12                                                          | 25165877  |
| b       | @        | 11                                                          | 23068770  |
| p       | @        | 11                                                          | 23068784  |

**注意：** 我们最初的输入文本在……之后有一个 `\hskip` 命令，但记号列表中没有表示该字符的记号。该字符被 LuaTeX 的输入扫描（读取）过程吸收了，因为它被用来终止 LuaTeX 对构成 `\hskip` 该命令。

## \directlua 的真正工作方式

既然我们已经探讨了记号、记号列表以及将记号转换为文本，接下来的挑战就是理解 TeX 引擎中“记号”的概念 *展开*.

如前所述， `\directlua{⟨code⟩}` 可以被要求处理 `⟨code⟩` 其中既包含 Lua 代码，也包含 TeX/LaTeX 代码，但 LuaTeX 内置的 Lua 语言解释器并不理解 TeX 或 LaTeX：那它是怎么工作的？怎么可能让 `⟨code⟩` 在不让 Lua 解释器被它不理解的命令彻底弄糊涂的情况下包含 TeX/LaTeX 指令呢？例如，下面这个 `\directlua` 命令只使用 TeX 宏，但它确实能工作：

```
\def\aa{tex}
\def\bb{.}
\def\cc{print}
\def\dd{("Hello")}
\directlua{
   \aa\bb\cc\dd
}
```

这个 `\directlua` 命令会让 LuaTeX 排版 `你好` 但这为什么、又是如何工作的呢，因为 Lua 语言并不理解 TeX 宏？

答案就包含在我们之前借用自 [LuaTeX 参考手册](http://www.pragma-ade.com/general/manuals/luatex.pdf) 不过我们可以认为 `\directlua{⟨code⟩}` 其工作方式是 LuaTeX 先对 `⟨code⟩` 在任何内容被传递给 Lua 解释器之前先进行“预处理”。这种“预处理”的本质——也就是它究竟意味着什么，以及它对你的 `⟨code⟩`——是我们接下来要讨论的主题，以帮助有兴趣的读者利用 `\directlua`.

### LuaTeX 如何处理 \directlua：初步了解

为了进一步加深我们对 `\directlua`的“预处理”活动的理解，我们可以从下面这个简化图开始，它概述了发生的过程。该 `⟨code⟩` 被提供给 `\directlua{⟨code⟩}` 首先使用上述讨论的过程和计算将其转换为记号；该记号序列存储在记号列表中。记号列表构建完成后，其中的每个记号都会转换回其文本表示：每个记号——字符记号或命令记号——产生的文本都会被合并（连接）起来，形成一段单独的代码字符串，然后传递给 Lua 解释器执行。

![](/files/99cab92cbbe3ee2c84a279399636e4bd6bd9161d)

等等，把文本转换为记号，再把这些记号直接转回文本，这样做有什么意义呢？你可能不会感到惊讶：没错，我们在这张图里还没有包含一个额外且至关重要的过程： *记号展开*。由你的 `⟨code⟩` 生成的每个记号都会经过一种“检查”，LuaTeX 会测试该记号是否表示一个属于以下子集的命令： *可展开命令*。如果是，LuaTeX 就会通过以下方式将该命令过滤掉： *移除* 从你的输入中将其 `⟨code⟩` 和 *替换为* 一个过程的结果，该过程在 TeX 引擎中称为 *记号展开*.

### LuaTeX 如何处理 \directlua：再看展开

TeX 的展开机制是所有基于 TeX 的排版引擎的核心组成部分，因为归根结底，它们每一个都源自（或基于）Knuth 最初的 TeX 源代码和设计。不过，展开这一概念很难用简洁而又易于理解的语言来解释，因为在实际中，展开是一个“总称”，用于描述一个单一过程——但这个过程会产生多种输出。这些不同的结果，是因为可以应用展开的命令集合有些杂，所以你可以把每个可展开命令看作具有某种“展开行为”。

作为 *初步近似* 来理解展开，我们可以说，一个记号（命令）的展开意味着 *移除* 从 TeX 当前输入中移除该命令（记号），并 *将其替换为* 用执行该特定可展开命令所产生的一串记号来替换它——也就是用其展开的结果/后果替换原始记号 *行为*。不过，这个关于展开的初始“定义”——即为 TeX 生成新的记号以供读取——并不完全适用于所有可展开命令，但作为起点已经足够了。

举个直接的例子：TeX 原语 `\jobname` 是一个可展开命令，而它的 *展开* 是一串字符记号，表示主 TeX 输入文件的名称。如果 TeX 决定展开一个 `\jobname` 命令（记号），它就会被 *移除* 出 TeX 当前输入源，并 *被替换* 为它生成的字符记号序列——随后 TeX 会继续读取/处理这些记号。

在 `\directlua`中，一个可展开记号在被处理（移除）并替换为新记号后，LuaTeX 会继续读取它刚刚放入的位置上的新记号——但其中一些新记号也可能是可展开的。由于 `\directlua` 执行所谓的 *完全展开*，LuaTeX 会读取这些新记号，并再次通过展开过程去展开（移除）任何新的（可展开的）记号——这个展开过程会持续进行，直到不再有可展开记号为止。不过，这条“持续展开”规则有两个重要例外，我们将在下面讨论：

* 使用构造 `\the\toks`;
* 有意防止（抑制）一个或多个选定记号的展开。

如前所述，我们用于理解展开的工作定义（初步近似）并未涵盖可展开命令子集所展示的全部展开行为。例如，有些可展开命令不会像 `\jobname` 那样生成记号，但它们可能会：

* 从输入中“过滤”记号：TeX 引擎的条件命令（`\if`, `\ifcat`, `\ifnum`, `\ifdim`, `\ifodd`, `\ifvmode`，…）都是可展开的。它们的展开行为是一种“记号过滤”——条件命令可用于 `\directlua`.
* “摆弄”输入中的记号： [`\expandafter` 命令](/latex/zh-cn/shen-du-wen-zhang/03-a-six-part-article-series-on-expandafter-tex-tokens-and-expansion.md) 是可展开的，并会改变两个记号的展开顺序。
* 阻止展开：可展开命令 `\noexpand` 和 `\unexpanded` 会抑制输入中命令记号的展开。
* 将输入中的字符序列转换为命令记号： `\csname … \endcsname。`
* 将内部量转换为一串字符记号： `\number` 和 `\the` 是可展开命令，它们会生成一串字符记号，表示某个内部量的值。
* 将命令记号转换为字符记号： `\string` 和 `\detokenize` 是可展开命令，它们会将参数转换为类别码 12 的一串字符记号。请注意 `\detokenize` 不同于 `\string`: `\detokenize` 可处理多个记号，并且在处理由……创建的命令记号后，会插入一个类别码为 10 的空格字符 *控制字*。实际上， `\detokenize` 会在命令名后添加一个尾随空格字符——我们将在本文后面看到一些示例。

#### 进一步完善我们对展开的“定义”

现在我们可以将定义概括为：一个命令（记号）的展开涉及 *移除* 该命令（记号）从 TeX 当前输入源中被 *替换为* 并以……的结果 *记号操作* 所执行的结果来替换。实质上，展开过程会促使一个可展开命令对 TeX 当前输入中的记号执行某种“操作”，从而影响 TeX 随后将读取的记号数量或行为——这种“操作”的精确性质取决于正在展开的是哪一个命令。所有宏和活动字符都是可展开的，但 TeX 引擎内置命令（原语）中只有少数被归类为可展开——可展开命令列表取决于你所使用的 TeX 引擎。

每一种新的 TeX 引擎都会继承其前代所内置的原语命令——也就是它所源自的上一代 TeX 引擎——其中一些继承来的原语是可展开的。当然，新的 TeX 引擎也可能选择不实现旧引擎中包含的某些原语命令，或者修改它们的行为以适应新引擎的需要。此外，新的 TeX 引擎通常会实现额外的原语，以支持其自身增强的功能——其中一些也可能是可展开的。因此，你可用的可展开命令数量会因所使用的 TeX 引擎而异——LuaTeX 有相当多这类命令。

解释/理解展开的另一个难点，也许也是真正的挑战，在于准确知道 *何时* TeX 引擎究竟会不会真正执行展开过程。这是一个庞大而复杂的话题，因为展开深深嵌入 TeX 引擎内部运作的各个方面：除了在 `\directlua`.

### LuaTeX 如何处理 \directlua：最后一看

下图总结了 `\directlua` 在 LuaTeX 引擎内部发生的预处理活动。该图还展示了两个实际执行工作的底层（内部）LuaTeX 函数： `scan_toks()` 和 `tokenlist_to_cstring()`。这些函数是用 C 语言编写的，位于可执行的 LuaTeX 软件深处：它们属于 LuaTeX 的内部机制，而不是 *直接* 你的 TeX/LaTeX 代码可以直接访问的。

![](/files/b579751b07334a894e40a5375a2562e877a14f10)

下面对 `\directlua ⟨code⟩`的预处理活动的描述概括了上图。

1. 你在 ⟨code⟩ 中的字符序列由 `scan_toks()`处理。其目的是逐字符读取你的 ⟨code⟩，生成字符记号和命令记号。由于它是在创建记号，因此在读取时分配给 ⟨code⟩ 中每个字符的类别码极其重要。
2. 在 `scan_toks()`的记号处理（生成）过程中，任何可展开命令（记号）都会被展开， *除非* 除非通过以下命令加以阻止： `\protected` （宏定义）， `\noexpand`, `\unexpanded` 等等。活动字符（类别码 13）也会被展开（除非被阻止）。
3. 由……创建的记号流 `scan_toks()` 会被构建成一个长记号列表——该列表中的记号包括对你 `⟨code⟩`还要注意， `scan_toks()` *不会* 不会触发或导致任何表示不可展开命令的记号执行：这类不可展开记号只是被直接传递过去，并被纳入正在构建的记号列表中。
4. 当记号列表完成且所有展开活动结束后，该记号列表会由另一个名为 `tokenlist_to_cstring()` 的函数处理，该函数会把最终记号列表中的每个记号转换回其文本表示。这样就生成了一段文本字符串，也就是要传递给 Lua 解释器的 Lua 代码。要成功执行，该字符串需要包含语法正确的 Lua 代码。
5. Lua 对该代码的处理分两步进行：
6. LuaTeX 内置的 Lua 解释器会解析并“编译”前面步骤生成的 Lua 代码。如果解析/编译失败，Lua 解释器会产生错误（例如语法错误）——除非你选择在命令行中使用 `--interaction=nonstopmode` ，否则这些错误可能会导致 LuaTeX 运行失败。
7. 如果解析/编译成功，Lua 解释器就会执行步骤 (5a) 中编译好的代码。

本质上， `scan_toks()` 函数是 LuaTeX 预处理活动的核心：它的主要任务是展开你 `⟨code⟩` 的文本中包含的所有可展开 TeX/LaTeX 命令，并将它处理过的所有内容构造成一个记号列表。再次强调， `scan_toks()` *并不会执行不可展开命令* （记号）：它只是 *存储* 这些记号到它正在构建的记号列表中。完成后，该记号列表随后会被转换 *回文本表示* 通过 `tokenlist_to_cstring()`——记号列表是一个仅属于 TeX 的概念，对 Lua 解释器来说完全陌生，因此需要将其转换为文本，变成可传递给 Lua 解释器的 Lua 代码。

## 展开作为一种编程语言“接口”

你可以把 `\directlua`的展开过程看作一种机制，或接口，用于将数据/信息从“TeX 世界”传递到“Lua 世界”：为 TeX 语言向 Lua 语言传递数据提供一种方法。例如，像下面这样的 TeX 代码 `\number\count75` 可用于将存储在计数寄存器 75 中的“TeX 世界”值传递到“Lua 世界”的整数变量 x：

```
\count75=1564 % Data existing in the "TeX World"
\directlua{
   local x=\number\count75 \space % Transfer TeX data to the "Lua World"
   tex.print("x= "..x)
   local y = (2*x-65)/5
   tex.print(" and y = "..y)
}
```

这会生成 Lua 代码

```
 local x=1564 tex.print("x= "..x) local y = (2*x-65)/5 tex.print(" and y = "..y)
```

**注意**：我们添加了 `<space>\space` 在 `\number\count75` 以确保在 `1564` 和 `tex.print`——这里并非绝对必要，因为即使省略它，Lua 仍然可以正确解析代码。紧跟在 `\count75` 之后的空格字符会在 TeX 引擎查找数值时被吸收——这里，它是提供给 `\count`。紧随其后的空格字符 `75` 用于终止 LuaTeX 对数字序列 `75` 的查找，并从输入中吸收。该 `\space` 宏展开后提供所需的空格字符，用于分隔文本 `1564` 和 `tex.print`.

使用上面的代码，LuaTeX 将排版

`x= 1564 and y = 612.6`

这里，“数据传递”机制是通过 `\number`：一个可展开命令，在此例中它指示 TeX 取回存储在 `\count` 寄存器中 `75` 中的值，并从该值（`1546`）生成一系列字符记号，每个数字对应一个字符记号，从而得到数字的记号序列 `1`, `5`, `6` 和 `4`。这 4 个字符记号会被纳入由……构建的主记号列表 `\directlua` ，并在记号列表转换为文本时再转换回其文本表示。毫无疑问，这是一条非常迂回的路径：从 `\count75` LuaTeX 内部存储的寄存器值，到最终进入 Lua 代码的数字；但归根结底，它确实可行。

**提示：** 如果你想检查 LuaTeX 展开活动的结果，可以写出如下代码：

```
\directlua{
   local foo=[[local x=\number\count75
   tex.print("x= "..x)
   local y = (2*x-65)/5
   tex.print(" and y = "..y)]]
   print(foo)
}
```

在这个例子中，我们使用长括号方法创建一个字符串变量 `foo` 其用途是保存通过展开介于……之间的所有内容而生成的 Lua 代码字符串 `[[` 和 `]]`。该字符串通过 Lua 函数调用输出到控制台： `print(foo)`.

在 Overleaf 上，你可以通过将……的内容写入来查看类似结果 `foo` 添加选项 `.log` 文件，使用 LuaTeX 的 Lua 函数 `texio.write()`:

```
\directlua{
   local foo=[[local x=\number\count75
   tex.print("x= "..x)
   local y = (2*x-65)/5
   tex.print(" and y = "..y)]]
   texio.write(foo)
}
```

## \directlua 记号列表中的记号：不可展开记号和未展开记号

我们已经指出， `\directlua{⟨code⟩}` 执行 *完全展开* 你的 `⟨code⟩`：它会移除并展开所有可展开命令，直到只剩下不可展开记号。由 `\directlua`的处理过程（在 `scan_toks()` 函数中）被串联起来形成一个记号列表，其中的每个记号都会被转换回文本，以便传递给 Lua。

不过，我们还没有讨论这个故事的最后一部分，因为我们需要考虑能够通过并进入正在构建的记号列表中的两类命令记号，这些记号位于 `\directlua`：我们将把它们称为 *简写命令记号* 和 *未展开* 记号：

* **简写命令记号**：这种命令记号来自使用下列某个 TeX 原语定义的控制序列 `\chardef`, `\mathchardef`, `\countdef`, `\dimendef`, `\skipdef`, `\muskipdef` 和 `\toksdef`。这些原语命令用于定义表示数值的控制序列——得到的控制序列是 *不* 可展开的。
* **未展开记号**：这种记号类型来自通常本应展开、但 `\directlua` 已经被以下方式之一处理：
* 被明确指示 *不* 对它们进行展开；例如，通过以下命令抑制展开 `\noexpand` 或 `\unexpanded`——我们稍后会说明这是如何做到的；
* 通过处理序列注入记号 `\the\toks` （下文会进一步说明）。

### \directlua 记号列表中的两“类”记号

根据我们的讨论，我们可以说，在第一阶段构建的记号列表中所包含的记号属于 `\directlua`的预处理（在 `scan_toks()` 函数中）可分为两组：

1. *本质上不可展开的* 记号

* 任何表示非活动 *@*;
* 任何表示不可展开 *原语* *命令*;
* 任何表示 *简写命令* （这些不是可展开的，见下文）。

3. *未展开* 记号：

* 任何表示可展开命令、其展开已被 *抑制* （或避免）在 `\directlua`的预处理过程中。

#### 简写命令记号：创建不可展开命令

如前所述，TeX 引擎提供了一组原语（内置命令），可用于构造 *不可展开的* 控制序列（这里用 `⟨command⟩`表示）。这些原语的形式如下：

* `\chardef ⟨command⟩ = ⟨numeric value⟩`
* `\mathchardef ⟨command⟩ = ⟨numeric value⟩`
* `\countdef ⟨command⟩ = ⟨numeric value⟩`
* `\dimendef ⟨command⟩ = ⟨numeric value⟩`
* `\skipdef ⟨command⟩ = ⟨numeric value⟩`
* `\muskipdef ⟨command⟩ = ⟨numeric value⟩`
* `\toksdef ⟨command⟩ = ⟨numeric value⟩`

其中 `⟨numeric value⟩` 是适用于各个命令的某个整数值。

这里，我们简要回顾 `\chardef` 的用法，以展示这些原语的关键特性——产生一个 `⟨command⟩` ，它是不可展开的。你可以使用 ``\chardef\mydollar=`\$`` 来创建控制序列 `\mydollar` 并用它排版一个 `$`:

`I paid \mydollar30.`

这将排版出 `I paid $30.` 该控制序列 `\mydollar` 由……创建的 `\chardef` 不可展开，下面的示例可以看出这一点：

```
\chardef\mydollar=`\$
\directlua{
   local x =[[I paid \mydollar30.]]
   texio.write(x)
}
```

这会在 `.log` 文件

`I paid \mydollar 30.`

这表明 `\mydollar` 由 *不* 在……期间被展开 `\directlua`的预处理过程中。出现在……后面的空格 `\mydollar` 是在命令记号转换为其文本表示时添加的。

当你使用 `\chardef` 来创建控制序列时，TeX 对该控制序列（命令）的内部分类会使其成为 *不可展开的* ，这与由宏定义命令 \def、\edef、\gdef 或 \xdef 定义的控制序列有很大不同。正如上文所述，在构建其记号列表的过程中 `\directlua` 会检查每一个传入的命令记号以判断其是否可展开。如果一个命令记号不可展开，它就会直接通过并进入记号列表，而其文本表示随后会在将记号列表中的记号转换回文本所生成的 Lua 代码字符串中再次出现。

**关于 plain TeX 与 LaTeX 的简短说明**

从历史上看，Knuth 最初的 plain TeX 定义了常用的控制符号 `\%`, `\&`, `\#` 和 `\$` 使用 `\chardef`——而不是使用标准的宏定义命令之一 `\def`, `\edef`, `\gdef` 或 `\xdef`。例如：

```
   \chardef\#=`\#
   \chardef\$=`\$
   \chardef\%=`\%
   \chardef\&=`\&
```

奇怪的 `` `\ `` 语法是 TeX 获取数值字符代码值的一种方法。在旧的 plain TeX 体系中，这些控制符号不可展开（由于 `\chardef`）但 LaTeX（或宏包）可能会将它们重新定义为 *宏* 以提供增强功能——这会使它们变为可展开，因此你可能需要注意这一点。

**这会如何影响 \directlua？**

让我们比较以下代码在 plain TeX 和 LaTeX 下运行的结果。为简单起见，我们将结果写入 `.log` 文件，使用 LuaTeX 的 Lua API 函数 `texio.write()`.

```
\directlua{
   local x=[[\$150 for the "\#1" product---20\%! more than its competitor, Widget \& Co.]]
   texio.write(x)
}
```

使用 **plain TeX** 运行这段代码会在 `.log` 文件中产生以下输出，显示任何展开的结果：

```
\$150 for the "\#1" product---20\%! more than its competitor, Widget \& Co.
```

显然，在 plain TeX 下，这些控制符号都没有`\$`, `\#`, `\%` 或 `\&` 被展开——因为它们都是通过 `\chardef`.

使用 **LaTeX** 文档运行这段代码：

```
\documentclass{article}
\begin{document}
   \directlua{local x=[[\$150 for the "\#1" product---20\%! more than its competitor, Widget \& Co.]] texio.write(x)}
\end{document}
```

运行这段代码会在 `.log` 文件

```
\protect \TU\textdollar 150 for the "\#1" product---20\%! more than its competitor, Widget \& Co.
```

显然，运行 LaTeX 产生的结果与 plain TeX 不同，因为在 LaTeX 下，命令 `\$` 已经被展开，这表明它是一个宏。

**注意：** 在 plain TeX 和 LaTeX 中 `\directlua` 都没有完全处理任何控制符号 `\%`, `\&`, `\#` 和 `\$` 来生成相应字符。在 `\directlua` 表示这些控制符号的记号——或者在 LaTeX 中，它们的展开结果——会直接通过并进入正在构建的主记号列表。

**注意：** 控制符号由一个类别码不是 11 的单个字符构成，例如 `\#`。当表示控制符号的记号被转换回其文本表示时，TeX 引擎不会在该文本后插入空格字符。这种对控制符号的特殊处理是 TeX 引擎运作的一条内置规则。

### 未展开记号：抑制展开

`\directlua`的预处理是一个例子，在这里 TeX 引擎正在执行展开，但你可能想要 *阻止* 将展开应用到一个或多个本来会被展开的记号上。再举一个例子，LuaTeX（以及所有 TeX 引擎）都会执行一个展开过程，类似于 `\directlua`，当它们处理 `\write` 命令：

`\write file-number {⟨material⟩}`

\write 指示 TeX 引擎输出 `⟨material⟩`——通常包含 TeX/LaTeX 命令——到一个文本文件（`file-number`）；其中的任何可展开命令 `⟨material⟩` 如果不加阻止，将在 `⟨material⟩` 实际写入该文件之前被展开。

如你所料，TeX 引擎提供了用于抑制或控制展开的命令：

* `\noexpand⟨token⟩`：阻止单个 `⟨token⟩`;
* `\unexpanded{⟨material⟩}`：阻止 `⟨material⟩`中所有可展开命令（记号）的展开。它实际上是 `\noexpand`;
* `\protected`的多记号版本：这是添加到宏定义前的一个前缀，可在某些情况下（例如在 `\directlua`, `\write` 或 `\edef`).

尽管名称似乎暗示了其他含义，但二者 `\noexpand` 和 `\unexpanded` 已 *可展开命令* 和 都很好地说明了把 TeX 引擎的展开过程视为执行“记号操作”：这里的操作是阻止一个或多个后续记号（命令）的展开。因为 `\noexpand` 和 `\unexpanded` 二者都是可展开命令，所以它们会在 `\directlua`的预处理过程中被移除并处理（执行），因为它在从你的 `⟨code⟩`.

#### \noexpand ⟨token⟩

`\noexpand ⟨token⟩` 阻止单个 `⟨token⟩`. `\noexpand` 在 `\directlua` 将被展开（从输入中移除）并替换为其“展开行为”的结果。展开 `\noexpand` 的结果是创建一个特殊的（隐藏的） `⟨marker token⟩` ，它被放在原始 `⟨token⟩` 之前，其展开将被抑制：该 `⟨marker token⟩` 充当一个标志，表示“不要展开下一个记号”。因为 `\directlua` 正在执行完全展开，它会重新处理由可展开命令的“展开行为”产生的任何记号。因此，当 `\noexpand ⟨token⟩` 的展开完成后，LuaTeX 会回过头读取结果，并看到两个记号的序列 `⟨marker token⟩⟨token⟩` ，这会使原始的 `⟨token⟩` 以未展开的形式通过，进入由 `\directlua`.

**示例**

如果我们写

```
\directlua{
   local x= "\\TeX"
}
```

该 `\TeX` 宏会被展开成其组成记号，在 plain TeX 中，这会导致以下文本传递给 Lua（注意：Lua 不能处理这段代码，它只是一个用于演示该过程的示例）：

`local x = "T\kern -.1667em\lower .5ex\hbox {E}\kern -.125emX"`

如果我们 *抑制* 对 `\TeX` 宏的展开，使用 `\noexpand`

`\directlua{local x= "\noexpand\TeX"}`

则会生成以下 Lua 代码（同样，Lua 不能运行这段代码；它只是一个用于演示 `\noexpand`):

`local x= "\\TeX "`

由于 `\noexpand`, `\directlua` 不会展开 `\TeX` 而只是允许表示该 `\TeX` 命令的记号毫发无损地通过，进入在第一阶段 `\directlua`的预处理过程中。

**注意：** 之后出现的空格字符是 `\TeX` 由 LuaTeX 随后将 `\TeX` 整数记号值转换回其文本表示形式（在 `tokenlist_to_cstring()` 函数中）引入的。

#### \unexpanded{⟨material⟩}

`\unexpanded` 是一个可展开命令，它会抑制由 `⟨material⟩`形成的所有记号的展开。正如我们已经指出的，当 TeX 引擎执行展开时，任何可展开命令都会 *移除* 从输入中 *被替换* 并由其“展开行为”的结果替换；那么这对于 `\unexpanded`意味着什么呢？通常，在 *完全展开*中，一旦某个命令的展开过程完成，TeX 引擎就会继续读取/处理由该命令的“展开行为”产生的任何记号——它需要进一步展开所生成的任何记号。然而， `\unexpanded` *绕过* 任何进一步的展开：它是这样做到的。

在 TeX 引擎内部， `\unexpanded` 命令首先将其中的字符和命令转换为 `⟨material⟩` 一个由 *未展开* 个记号组成的临时记号列表。所有记号创建并存储到该临时记号列表之后， `\unexpanded` 命令会使 `\directlua` 更改为 *跳过* 回过头去读取并处理它们——尽管 \directlua 正在执行完全展开。相反，这些 *未展开* 记号会直接通过，并被并入由 `\directlua` （在 `scan_toks()` 函数中）构建的主记号列表。这样， `⟨material⟩` 中的一切都被转换为记号，而这组记号的展开过程被跳过。 `\unexpanded{⟨material⟩}` 的操作类似于 `\the\toks`，我们将在下文讨论。

**示例**

`\unexpanded` 产生结果的方式类似于 `\noexpand` ，只不过它可以阻止多个记号的展开；例如：

```
\directlua{
   local x = "\unexpanded{\foo\bar\foobar}. But Lua can't process this code!"
}
```

它会生成以下文本作为 Lua 的代码：

`local x = "\foo \bar \foobar . But Lua can't process this code!"`

**注意**：每个命令名后面都有空格字符。这同样是 LuaTeX 随后将未展开的记号 `\foo`, `\bar` 和 `\foobar` 转换回文本时造成的结果，在 `tokenlist_to_cstring()` 函数中。

#### 受保护的宏定义

该 `\protected` 命令是一个前缀，应用于宏定义，以防止该宏在 TeX 构建展开的记号列表时被展开，例如由 `\directlua`的预处理过程中。

**示例**

假设你使用和不使用以下前缀来定义下列宏： `\protected` 前缀：

```
\def\macroA{\"This unprotected macro contains a string\"}
\protected\def\macroB{\"This protected macro also contains a string\"}
```

如果你使用 Lua 的字符串连接运算符（`..`）来写

```
\directlua{
   local x=\macroA..\macroB
}
```

`\directlua`的预处理会生成以下代码传递给 Lua：

`local x="This unprotected macro contains a string"..\macroB`

`\macroA` 没有使用 `\protected` 定义，因此它会被展开，生成要连接的字符串的第一部分，但 `\macroB` 是使用 `\protected` 定义的，因此它没有被展开。

在预处理过程中，LuaTeX 的 `scan_toks()` 函数为 `\macroA`创建了一个记号，识别出它是一个普通的可展开命令并将其展开：该展开生成一串字符记号，每个字符对应一个字符记号，分别对应于 `"This unprotected macro contains a string"`。每个字符记号都会继续传递并添加到正在构建的记号列表中。

当 `scan_toks()` 为 `\macroB` 创建记号时，它注意到该命令被定义为 `\protected` 并且不展开它：表示 `\macroB` 的记号会原样通过（未展开）并进入正在构建的记号列表。该记号列表构建完成后，预处理的下一阶段是在 `tokenlist_to_cstring()` 函数中将记号列表里的所有记号转换回它们的文本表示形式。表示 `\macroB` 的未展开记号会被检测到并转换为其文本表示，从而生成 `\macroB` 出现在准备传给 Lua 的代码中的文本。注意，Lua `"This unprotected macro contains a string"..\macroB` 来生成最终字符串，因为 `\macroB` 在 Lua 语法中没有意义，因此会导致错误 `unexpected symbol near '\'`.

**趣闻**： `\protected` 命令是由 $$\varepsilon\text{-}\mathrm{\TeX}$$引入的，这是 Knuth 原始 TeX 软件的第一个主要扩展，并且所有其代码谱系包含 $$\varepsilon\text{-}\mathrm{\TeX}$$.

### 未展开记号：在 \directlua 中使用 \the\toks

编程生活若没有那些需要处理和使用的“特殊情况”就不会一样，而将 `\the` 与 `\toks` 结合使用在 `\directlua` 命令中就是这样一个特殊情况。

#### 关于 \toks 的简要背景

TeX 原语 `\toks` 指示 TeX 引擎保存一些记号以供日后使用：这些记号不会继续传递进行进一步处理，而是被放在一边并存储到使用 *记号寄存器*指定的一个内存位置中。例如，我们可以告诉 TeX 引擎创建一些记号并将它们存储在记号寄存器位置 `100` 使用

`\toks100={Hi, \TeX! \hskip 5bp}`

在这里，TeX 使用记号寄存器 `100` 来访问其内存中的一个已知位置：一个用于保存记号列表的存储区域。

表示介于以下内容之间的一切的记号 `{` 和 `}` 都会被创建， *但不会展开*，并串联成一个记号列表——类似于我们在本文前面探讨过的记号列表。要重复使用这些记号，我们会写 `\the\toks100` 其中 `\the` （一个可展开命令）指示 TeX 取出已存储的记号，并将它们插入到你写下 `\the\toks100`的位置。另一种理解方式是 `\the\toks` 会让 TeX 在该位置插入一些记号。

该 `\toks` 命令 *不会展开* 它被要求创建和保存的任何记号：它只是将介于 `{` 和 `}` 之间的字符和命令转换为记号并将它们存储起来。

#### 回到 \directlua

在讨论展开时，我们指出 `\directlua{⟨code⟩}` 执行 *完全展开* 的记号值 `⟨code⟩`：移除所有可展开命令，并用其展开行为的结果替换它们——继续 *进一步展开* 由可展开命令初始展开产生的任何记号。

`\the` 是一个可展开命令，因此 `\directlua` 会展开它；然而，当 `\the` 与 `\toks` 在 `\directlua`结合使用时，如 `\the\toks⟨token register⟩`，插入的记号 *不会再继续展开*。 `\the\toks⟨token register⟩` 的展开会将存储在 *未展开* 中的一串 `⟨token register⟩`里的记号直接注入到由 `\directlua`构建的记号列表中：这种行为绕过了通常的完全展开过程。实际上，这些记号会通过， *未展开*，并被并入由 `\directlua`——这种未展开记号的透传过程在运作上类似于 `\unexpanded`，如前所述。

**示例**

假设我们定义宏 `\mymacro` as `\def\mymacro{\TeX}`。它只包含一个 `\TeX` 命令的记号（它是一个宏）：因此我们有一个可展开命令 `\mymacro` ，其中包含另一个宏 `\TeX`，而它也是可展开的。

下面的代码会导致 Lua 尝试创建一个字符串变量 `x`:

```
\def\mymacro{\TeX}
\directlua{
   local x="\mymacro"
}
```

在 \\`directlua`中，表示 `\mymacro` 的记号会被展开，但这会产生另一个可展开记号， `\TeX`，该记号会继续展开。在 plain TeX 中，这些展开会导致以下文本传递给 Lua：

`local x = "T\kern -.1667em\lower .5ex\hbox {E}\kern -.125emX"`

这段代码试图定义一个字符串，其中包含表示该 `\TeX` 宏展开版本的文本。如果你尝试运行这个示例，Lua 会尝试构造该字符串，但会失败并产生错误：

`invalid escape sequence near ' "T\k'.`

本文后面我们会探讨“invalid escape sequence”的含义。

现在让我们将 `\mymacro` 与将 `\TeX` 记号放在由 `\toks` 命令：

```
\toks100={\TeX}
\directlua{
   local x="\the\toks100"
}
```

LuaTeX 的 `\directlua` 处理会为 Lua 生成如下文本字符串：

`local x = "\TeX "`

在 `\TeX` 之后的空格字符是由 LuaTeX 的命令记号到字符串转换过程生成的。

**但请注意**： `\TeX` 宏已经 *不* 展开为其组成记号。 `\the\toks100` 使存储在寄存器 100 中的记号被插入，但仅此而已：它们 *不* 不会再进一步展开，而是并入由 `\directlua` （在函数 `scan_toks()`中）构建的主记号列表。将记号放入由 `\toks` 创建的记号列表中，也是防止记号展开的另一种方式。

如果我们运行这个示例，它同样会产生错误：

`invalid escape sequence near ' "\T'.`

我们会在本文后面探讨 Lua 的转义序列。

## 展开中使用的其他命令/技术

在本节中，我们看看一些额外的 TeX 命令/方法，它们在应用展开的情况下可能很有用（例如在 `\directlua`).

### \string ⟨token⟩

`\string` 是一个可展开命令，会将 ⟨token⟩ 转换为一系列字符记号，每个记号的类别码都是 12。

例如， `\string\TeX` 会产生一系列 4 个字符记号 `\`, `T`, `e` 和 `X` 其中每个字符都被赋予类别码 12（包括前导 `\` 字符）。

如果我们写

```
\directlua{
   local x="I will use \string\newcommand"
   print(x)
}
```

该 `\string` 命令会被展开，生成一串类别码为 12 的字符记号。在 `\string` 展开后，生成的字符记号（表示 `\newcommand`中的每个字符）会被并入由 `\directlua`。一旦 `\directlua` 完成主记号列表的构建，它的组成记号会被转换回文本表示，从而生成以下要传递给 Lua 解释器的代码：

`local x="I will use \\newcommand" print(x)`

当这段代码传递给 Lua 时， `print(x)` 会输出字符串 `x` 到屏幕（控制台）。不过，我们稍微耍了个小花招，故意使用了一个以 `\n`开头的示例命令。如果你能在本地 TeX 安装中运行这个示例，你会注意到 Lua 会将以下文本打印到屏幕：

```
   I will use
   ewcommand
```

要在 Overleaf 上运行这段代码，你可以指示 LuaTeX 直接写入 `.log` 文件，使用 LuaTeX 的 Lua API 函数 `texio.write(*string*)`:

```
\directlua{
   local x="I will use \string\newcommand"
   texio.write(x)
}
```

如果你检查生成的 `.log` 文件，你会看到其中也包含

```
   I will use
   ewcommand
```

这个意外输出是由于 Lua 将 `\n` 开头的 `**\n**ewcommand` 解释为换行字符的转义序列（字符代码 10）：它会假定你想开始一个新行的文本，并以 `ewcommand`。我们将在本文后面讨论 Lua 的转义序列。

### \detokenize{⟨material⟩}

`\detokenize` 在效果上是 `\string` 的多记号版本，它也是一个可展开命令，会将 `⟨material⟩` 中的一切转换为类别码为 12 的字符记号序列——*只是* 空格字符（ASCII/Unicode 值 32）除外，它们的类别码是 10。 `\detokenize` 在命令名后面也会插入一个尾随空格字符，这些命令名是 *控制字* （例如， `\foo`））但不会在 *控制符号* （例如， `\#`, `\%` 等）之后插入空格字符。

### 示例

即使宏 `\foohoo`, `\foo`, `\bar` 和 `\foobar` 未定义，如果你写下：

```
\directlua{
   local x = "\string\foohoo\detokenize{\foo\bar\foobar}"
}
```

它会生成以下文本作为要传递给 Lua 解释器的代码

`local x = "\foohoo\foo \bar \foobar "`

如果你不使用 `\string` 和 `\detokenize` 并写：

`\directlua{local x = "\foohoo\foo\bar\foobar"}`

`\directlua` 会处理 `\foohoo`，识别出它是一个命令并尝试展开它；但由于 `\foohoo` 未定义，这会导致错误：

```
   ! Undefined control sequence.
   l.1 \directlua{local x = "\foohoo
                      \foo\bar\foobar"}
         ?
```

因为 `\string` 和 `\detokenize` 将它们的参数转换为一串字符记号， `\directlua`的展开过程确实有机会检测到可展开的命令记号 `\foohoo`, `\foo`, `\bar`，或 `\foobar`：在它们有机会触发展开之前，它们就已经被转换成了字符记号序列。

如前所述，命令的展开涉及将其从输入中移除，并用其“展开行为”的结果替换它。展开的结果（通常是记号）随后由 TeX 引擎读取。这里， `\string` 和 `\detokenize` 的“展开行为”是从输入中吸收字符和命令记号，并将它们转换为字符记号序列，最初存储在一个临时记号列表中，随后 `\directlua` 读取。那些字符记号会被并入由 `\directlua`.

下图描绘了 `\string` 如何将 `\foohoo` 命令转换为一串字符记号，从而生成一个临时记号列表，随后由 `\directlua` 读取，以便将这些字符记号并入正在构建的主记号列表。

![](/files/faff7a30cbe2828ad38d4183c2340bab9dbe6837)

如果 `\string` 或 `\detokenize` 在其参数中遇到字符，例如 `\string a` 或 `\detokenize{abc}` 这些字符（此处类别码为 11）会生成字符记号，但类别码为 12。

注：

如果我们回到上面的例子：

`\directlua{local x = "\string\foohoo\detokenize{\foo\bar\foobar}"}`

它会生成以下文本作为要传递给 Lua 解释器的代码

`local x = "\foohoo\foo \bar \foobar "`

我们可以观察到以下几点：

* `\detokenize` 在每个宏名后都插入了一个空格字符，但 `\string` 没有。
* `\string` 只作用于单个记号。
* 在字符串 `"\foohoo\foo \bar \foobar "` 中，用来定义 `x` 时，我们将再次遇到 Lua 的转义字符机制（如下所述）：

  * `\bar` 以 `\b` 开头，这是 Lua 中用于表示 [退格字符](https://en.wikipedia.org/wiki/Backspace) （字符代码 8）；
  * 命令 `\foohoo`, `\foo` 和 `\foobar` 都以 `\f`开头，这是 Lua 中用于表示 [换页字符](https://en.wikipedia.org/wiki/Page_break#Form_feed) （字符代码 12）。

  因为字符序列 `\b` 和 `\f` 在使用双引号创建的字符串中使用 `"..."` 它们会产生不希望的结果，除非采取措施通过 Lua 所谓的 *长方括号* 字符串方法：我们现在可以把它与 Lua 转义序列一起讨论了。

## 什么是“Lua 转义序列”？

编程语言会保留某些字符供“特殊用途”作为语言语法的一部分：实际上，这些字符被定义为具有某种特殊含义。不过，有时你需要暂时“关闭”某个字符的特殊含义，例如当你希望该字符作为更长字符串的一部分嵌入时，而它的标准行为会引入语法错误。简而言之，这个字符需要被处理 *没有* 使其标准解释生效——悄悄地、不被察觉地通过。为此，程序员使用一种称为 *转义* 的方法，其中一个“特殊字符”由其所谓的 *转义序列*.

一个标准示例（Lua 也支持）是在字符串中使用双引号，此时可以用转义序列 `\"`:

`"当被问到 LuaTeX 时，他们回答：\"它是一款很棒的 TeX 引擎！\" 我表示赞同。"`

Lua 语言提供了多种处理转义序列的机制：

* 标准序列，包括 `\n` (换行)、 `\r` (回车)、 `\\` (反斜杠)、 `\"` (双引号)、 `\t` (水平制表符)、 `\v` (垂直制表符) 以及 `\'` (单引号)；
* `\xXX`，其中 `XX` 是恰好两个十六进制数字的序列；
* `\ddd`，其中 `ddd` 是最多三个十进制数字的序列；
* 在本文撰写时（2019 年 8 月），最新版本的 LuaTeX 虽然尚未在 Overleaf 上提供，但它使用的是 Lua 5.3 版本，该版本引入了对 UTF-8 转义序列的支持： `\u{XXX}`。这种转义机制用于 UTF-8 编码的 Unicode 字符，其中 `XXX` 是一个由一个或多个十六进制数字组成的序列，用于表示该字符的码点。注意，外层的括号 `{ }` 是必需的。

### 控制转义序列

传统上，字符串使用双引号定义，如 `"这是一个字符串"`；在这样的字符串中你可以使用转义序列： `"这是一个字符串。\n我现在将从新的一行开始。"`。不过，Lua 还有第二种而且 *非常* 更方便的字符串定义机制：它所谓的 *长方括号* 机制，即通过将文本包在 `[[` 和 `]]`:

`[[I am a long brackets string]]`

在使用长方括号方法创建的字符串中，Lua 的字符转义机制会被 *关闭*：转义序列会被当作普通字符处理。例如，在字符串

`[[I am a long brackets\n string]]`

该 `\n` 中，转义序列不会被视为单个回车字符（ASCII 码 13），而是被视为两个普通字符： `\` 后跟 `n`.

### 为什么长方括号字符串如此有用？

正如我们稍后将要探讨的那样，LuaTeX 提供了一套专门的内置 Lua 函数，你可以将其与 `\directlua` 配合使用，以控制 LuaTeX 的排版行为。在这些众多函数中，有一个叫做 `tex.print(*string*)` 它允许你将 `*string*` 来自 Lua 代码的内容传回 LuaTeX 进行排版。一个非常简单的例子是：

`\directlua{tex.print("Hello, World!") }`

这将使 LuaTeX 排版 `你好，世界！`

该 `*string*` 用于 `tex.print(*string*)` 也可以包含表示 TeX 和 LaTeX 命令的文本，供 LuaTeX 处理。不过，TeX/LaTeX 命令以 `\` 字符开头，这在使用双引号创建的字符串中会产生问题，因为 Lua 会尝试解析字符串，检测到开头的 `\` 字符，并将其解释为转义序列的开头。当 Lua 试图处理该转义序列时，通常会失败，因为开头的 `\` 与许多 TeX/LaTeX 命令名称中的第一个字符组合起来，并不能构成 Lua 所认识的有效转义序列。例如，在处理如下字符串时： `"我喜欢 \LaTeX"` Lua 会看到 `\L` 并报出“invalid escape sequence”错误，这就是上面提到的错误原因。

#### 长方括号字符串来救场了！

使用长方括号方法创建（定义）字符串非常有用，因为尽管 TeX/LaTeX 命令以 `\` 字符开头，长方括号字符串方法会禁用（关闭）Lua 的转义序列机制。下面是一个简短示例，记住我们需要阻止宏被展开，例如使用 `\protected` 或 `\noexpand`.

假设我们定义一个 `\newtest` 像这样的宏

`**\protected**\def\newtest#1{The argument: #1}`

并在 `\directlua` 与 LuaTeX 的 Lua API 函数 `tex.print()`:

```
\directlua{
   tex.print("\newtest{Hello}")
}
```

由于使用了 `\protected`，宏 `\newtest` 不会被展开，这会导致传递给 Lua 的文本如下：

`tex.print("\newtest {Hello}")`

添加在……之后的空格字符 `\newtest` 以及在左花括号（`{`）之前 `\directlua`将命令记号转换回其文本表示时产生的副作用。

该代码被传递给 Lua，随后 Lua 执行 LuaTeX 函数 `tex.print()` 不过会出现一个问题，其表现形式取决于你所使用的字体。在 Overleaf 的 LaTeX 中，你会看到如下输出：

![](/files/c4d37b61ed853bd7192c860ac06b59a45284e9a8)

并在日志文件中看到警告：

```
   缺少字符：没有
   (U+000A) 在字体 [lmroman10-regular]:+tlig; 中！
```

在 plain TeX 中，你可能会看到类似这样的输出：

![](/files/a547b8869c98ca68954627eca9d2a7cdace72ed8)

在这两种情况下， `\newtest` 宏都不会被调用，而且输出也不是我们想要的。该错误由 Lua 的转义字符机制引起：在文本 `\newtest {Hello}` 宏名称以 `\n` 开头，Lua 将其识别为换行字符的转义序列，因此它会把 `\n` 替换为 ASCII 字符 10，十六进制为 0A。在 LaTeX 错误消息中， `U+000A` 是使用 4 位十六进制数字表示 Unicode 值的一种方式。

因为 `\n` 被转换为换行字符后，LuaTeX 不会看到宏调用，而是认为它被要求排版一段以 ASCII 字符代码 10 开头的文本：

`⟨ASCII 10⟩ewtest {Hello}`

根据所使用的字体，LuaTeX 可能能够，也可能不能排版 `⟨ASCII 10⟩` 字符，但其余文本会原样输出，而 `{` 和 `}` 会被当作一个组处理并且不打印。

Plain TeX 会给出不同的结果，因为默认字体是 Computer Modern Roman，它有一种奇怪的编码，当看到字符代码 10 时会排版出一个大写 Omega。

为了防止这些问题，我们需要使用长方括号字符串来阻止 Lua 的转义机制生效。正确的结果可通过以下方式得到：

`\directlua{tex.print([[\newtest{Hello}]])}`

这会产生如下截图所示的结果：

![](/files/784bdd210504ac8abe2fdd23d827f845a997be35)

### 可展开命令的展开与不可展开命令的不执行

在讨论展开时，我们指出这是一个 TeX 引擎会执行的过程，在该过程中它会 *移除* 当前输入中的一个可展开命令（记号），并 *将其替换为* 该可展开命令产生的结果。由于 \directlua 正在执行 *仅展开* 操作（以生成记号列表），因此它 *不会* 不会让 LuaTeX 的处理继续更进一步。一旦一个可展开命令被读取并完全展开，该展开的结果——其中通常包括不可展开的命令（记号）——就会被纳入正在构建的记号列表中，准备转换回文本后传递给 Lua。

这里有一个重要原则在起作用：在 *仅展开* 旨在生成记号列表的操作期间，包括 LuaTeX 在内的 TeX 引擎 *不会执行* 任何不可展开的原始 TeX 内建命令。

在 `\directlua{⟨code⟩}`的情况下，如果你的 `⟨code⟩` 的完全展开版本产生了，或者包含了不可展开的 TeX/LaTeX 命令，那么这些命令 *将会传递给 Lua* （以文本形式表示）。

#### 示例

下面是一个示例，用来说明不可展开的原语在仅展开处理期间（例如在 `\directlua`）内不会被执行。假设我们定义一个宏 `\setcountreg` 如下：

`\def\setcountreg#1#2{\count#1=#2\relax}`

**注意**：我们使用 `\relax` 在参数 `#2` 之后，以防止 LuaTeX 在扫描输入、寻找与参数 `#2`.

如果在 `\directlua`的情况下，我们后来这样运行该宏

```
   \setcountreg{100}{50}
   100 号计数寄存器中的值是 \the\count100。
```

它将输出

`100 号计数寄存器中的值是 50。`

在这种情况下，任何 TeX 引擎都会处理该宏—— `\setcountreg`展开宏、确定参数，并继续读取 *并执行* 宏替换文本（定义）中包含的命令。这里的结果是将 `50` 作为存储在寄存器 `\count100`.

不过，当 TeX 引擎正在执行 *仅展开* 操作时，就像在 `\directlua`中那样，它 *不会执行* 宏定义中包含的不可展开命令。

如果我们写

```
\def\setcountreg#1#2{\count#1=#2\relax}
\directlua{
   local x = [[\setcountreg{100}{50}]]
}
```

它会生成以下文本作为 Lua 代码：

`local x = [[\count 100=50\relax ]]`

上面生成的 Lua 代码表明，在 `\directlua` 该 `\setcountreg` 已经被展开，其参数已被识别并替换到相应的参数（`#1` 和 `#2`）中，但它不会再进一步：不可展开的原始 TeX 命令 `\count` 由 *未被执行* 在 `\directlua`的展开处理中。

不过，如果我们将得到的字符串 `x` *再传回 LuaTeX* 通过 `tex.print(x)` 像这样

```
\count100=50 % set \count100 to a starting value of 50
\def\setcountreg#1#2{\count#1=#2\relax}
\directlua{
   local x = [[\setcountreg{100}{250}]]
   tex.print(x)
}
100 号计数寄存器中存储的值是 \the\count100。
```

在 `\directlua` 完成后，输出将为

`100 号计数寄存器中存储的值是 250。`

这表明计数寄存器 `100` 现在确实包含了数值 `250`.

由上例生成的 Lua 代码是

`local x = [[\count 100=250\relax ]] tex.print(x)`

这段代码定义 `x` 为使用长方括号方法创建的字符串，该方法用于避免错误的转义序列导致的错误。如果我们使用双引号 `"..."` 来定义 x，那么字符组合 `\c` 开头的 `\count` 会触发错误： `在 ' "\c"' 附近出现无效的转义序列`.

LuaTeX 的 Lua API 调用 `tex.print(x)` 会使 LuaTeX 执行 TeX 代码序列 `\count 100=250\relax` 和 `\count100` 被赋予值 `250` 从排版输出中可以看出：

`100 号计数寄存器中存储的值是 250。`

#### 注意：宏与 LuaTeX Lua API

在上面的例子中，我们看到在 `\directlua`的预处理（展开）过程中，LuaTeX 并没有执行代码 `\count 100=250`，其中包含 `不可展开的` 原始命令 `\count`：要运行（执行）那段代码，我们必须 *把它传回 LuaTeX* 通过 `tex.print()`.

`\directlua` 只是 LuaTeX 执行仅展开处理以构造记号列表的一个实例。还有其他命令也会执行类似的展开处理和记号列表生成操作，例如 `\write` 和 `\edef`：这些命令在展开处理期间同样不会执行不可展开的原语。TeX 引擎在仅展开处理活动中构造记号列表时，不会执行不可展开的原语，这是一条普遍原则。

**将我们的宏改写为使用 LuaTeX Lua API**

我们可以重写 `\setcountreg` 宏，使用一个名为 `tex.setcount()`，从而避免使用 TeX 命令来更改存储在计数寄存器 `100`:

```
   \def\setcount#1#2{\directlua{tex.setcount(#1,#2)}}
   \count100=50
   计数寄存器 100 包含 \the\count100\par
   \setcount{100}{250}
   计数寄存器 100 现在包含 \the\count100\par
```

这段代码将排版：

```
计数寄存器 100 包含 50
计数寄存器 100 现在包含 250
```

这里我们使用 `tex.setcount()`，LuaTeX 众多 Lua API 函数之一，用于 *直接访问* LuaTeX 的内部数据存储区，以将数值 `250` 放入表示计数寄存器 `100`的内存位置中。实际上，我们已经 *绕过了* LuaTeX 标准的 TeX 引擎输入处理方法：读取输入、创建记号以及执行 TeX 原语命令。不过，这里有一个值得警惕的故事：通过使用 LuaTeX 的 Lua API 函数，仅展开处理活动 *可能会产生副作用*：会改变存储在 TeX 引擎内部的值，而这些变化仅靠纯 TeX/LaTeX 命令本不可能实现。

**示例：意外的副作用**

下面是一个示例，用来演示 *意外的* 副作用，这些副作用可能出现在使用 `\directlua`的宏中。假设我们写下如下代码：

```
\def\dochange{\directlua{tex.setcount(999,12345)}}
\edef\careful{\dochange}
\the\count999
```

运行这段代码将排版 `12345`!

怎么会这样？我们并没有 *明确地* 调用任何代码或宏把该值放入计数寄存器 `999`。还是说我们调用了？

我们定义了 `\dochange` 替换为一个 `\directlua` 命令，它使用 `tex.setcount()` 来存储数值 `12345` 到计数寄存器 `999`：在 TeX 代码中，它相当于 `\count999=12345`。然后我们使用标准 TeX 原语 `\edef` 来定义宏 `\careful`——正是对 `\edef` 的使用触发了意外的副作用。

`\edef` 会完全展开其参数：在这里，它检测到一个可展开宏 `\dochange` 并将其展开。 `\dochange` 宏使用了可展开命令 `\directlua` ，其中包含一个 Lua API 调用；因此，对 `\dochange` 的展开会导致展开 `\directlua` ，这又导致 `tex.setcount()` 被调用，从而改变计数寄存器中的值 `999`.

如果我们把 `\dochange` 重新定义为使用 TeX 命令：

```
   之前：计数寄存器 999 包含 \the\count999。\par
   \def\dochange{\count999=12345\relax}
   \edef\careful{\dochange}
   之后：计数寄存器 999 包含 \the\count999。\par
```

运行这段代码将排版

```
之前：计数寄存器 999 包含 0。
之后：计数寄存器 999 包含 0。
```

显然， `\count999`没有受到影响。 `\edef` 定义 `\careful` 它会展开 `\dochange` 但该展开只会产生不可展开的 TeX 原语：它们会 *未被执行* 而只是 *存储* 在构成 `\careful`.

作为补充说明，同样的原理也解释了为什么这会产生排版输出：

```
\def\dochange{\directlua{tex.print("Hello")}}
\edef\careful{\dochange}
```

## LuaTeX Lua API 简介

正如我们所见， `\directlua` 它不仅允许你编写传统的 Lua 代码，或 Lua 与 TeX/LaTeX 代码的混合体，还提供了一套额外的 Lua 函数（LuaTeX 专用），你可以使用这些函数（调用它们）来与 LuaTeX 排版软件的内部运作进行通信，或直接控制其内部运作。本文中我们已经使用了若干 Lua 函数， `tex.print()`, `texio.write()`, `tex.setcount()` 这些函数以及 *许多* 更多函数，都记录在 [LuaTeX 参考手册](http://www.pragma-ade.com/general/manuals/luatex.pdf) 其中相关函数组被称为 *库*.

你可以把这些 Lua 函数看作 LuaTeX 的 Lua API（**一个**应用 **P**编程 **I**接口），它们提供了用于构建复杂排版和文档工程解决方案的工具，通过使用 Lua 作为驱动来控制 LuaTeX 的排版行为。

如前所述，LuaTeX 将其 API 组织成一组它称为“库”的函数：这些函数按其用途或动作相关联。每组函数都旨在提供对 LuaTeX 内部过程、数据结构、数据存储以及排版算法某一特定方面的访问。LuaTeX 在内部由多个组件构成：软件库/工具（大多用 C 编写），它们不仅构成 TeX 引擎本身，还包括其他子系统，如 Lua、MetaPost、Kpathsea、FontForge、libpng 和 zlib。这些库被集成起来构成 LuaTeX 可执行软件的特性和功能，用户正是通过 Lua API 才能访问 LuaTeX 由这些多个软件组件的集成与协调所带来的功能。

## 一些示例与陷阱

在本节中，我们将展示一些进一步的示例，这些示例使用了本文提供的主题、概念和说明。

### 使用波浪号字符（\~）

Lua 语言使用 `~` 字符（称为 tilde，波浪号）作为其语法的一部分，包括用于执行“不等于”测试的语法；例如，要测试一个变量 `x` 不等于 `4` ，我们可以写：

```
   local x=3
   if x ~= 4 then
   print("x is not equal to 4")
   end
```

如果我们尝试通过 `\directlua`:

```
\directlua{
   local x=3
   if x ~= 4 then
   print("x is not equal to 4")
   end
}
```

运行这段简单的 Lua 代码，就会得到一个错误：

`[\directlua]:1: 'then' expected near '\'.`

这很奇怪，因为我们的代码是正确的：我们使用了 `'then'` 而且没有 `\` 字符出现在我们的代码中，那么到底哪里出了问题？要理解这一点，我们必须记住，对 TeX/LaTeX 而言， `~` 通常会被定义为类别码 13 的“特殊字符”：所谓活动字符，它们是迷你宏，因此会被展开。当 `\directlua` 检测到 `~` 字符时，它会通过 *将它去掉* 从输入中 *替换为* 并将其展开结果替换进去。使用 plain TeX，LuaTeX 生成并传递给 Lua 解释器的结果文本（代码）实际上并不包含该 `~` 字符，而是：

`local x=3 if x \penalty \@M \ = 4 then print("x is not equal to 4") end`

该 `~` 字符已经被 *移除* 和 *展开* 成其构成的命令——上面的 Lua 代码来自 plain TeX 对该活动字符的定义 `~`。现在我们可以看出为什么 Lua 会报错 `'then' expected near '\'`——它开始解析这段代码，但遇到了单词 `\penalty` 这对 Lua 来说毫无意义，于是产生了语法错误。

要修复这个问题， `~` 字符在……时需要具有安全的类别码 `\directlua` 正在处理你的代码；例如，我们可以临时将 `~` 的类别码改为 11（字母），方法是把代码包在一个组中：

```
\begingroup
\catcode`\~=11
\directlua{
   local x=3
   if x ~= 4 then
   print("x is not equal to 4")
   end
}
\endgroup
```

这段代码按预期工作，并且 `x is not equal to 4` 会打印到控制台。还有其他选项：我们可以使用可展开命令 `\noexpand` 或 `\string`.

#### 使用 \string⟨token⟩

我们可以使用 `\string` 于单字符 `⟨token⟩` `~` 它的类别码为 13（活动字符）； `\string` 如何将 `~` 字符来生成一个类别码为 12 的字符记号。如果我们这样做

```
\directlua{
   local x=3
   if x \string~= 4 then
   print("x is not equal to 4")
   end
}
```

就会生成我们所需的 Lua 代码：

`local x=3 if x ~= 4 then tex.print("x is not equal to 4") end`

#### 使用 \noexpand⟨token⟩

我们可以使用 `\noexpand~` 来阻止该活动字符展开 `~`

```
\directlua{
   local x=3
   if x \noexpand~= 4 then
   print("x is not equal to 4")
   end
}
```

未展开的 `~` 记号会传递到正在于……中构建的记号列表 `\directlua` 并在转换回文本后生成可工作的 Lua 代码。

### 使用 # 字符

在 Lua 语言中， `#` 字符可以用来获取表的长度。然而，如果我们尝试下面的代码

```
\directlua{
   local tbl = {}
   tbl[1] = "Hello"
   tbl[2] = "World"
   tex.print("Table length is "..#tbl)
}
```

我们可能会期望 LuaTeX 排版出

`Table length is 2`

但它会生成一个错误：

`\directlua]:1: attempt to get length of a number value`

出现这个错误是因为 `#` 字符通常具有类别码 6（宏参数）——而 `#` 字符在 TeX/LaTeX 中有两个用途：用于表示宏参数（`#1`, `#2`… `#9`）以及对齐模板中的替换文本（用于 `\halign` 和 `\valign`).

当 `\directlua` 在生成记号以构建其记号列表时，它会看到 `#` 类别码为 6 的字符，并创建一个合适的字符记号来表示它。当需要把最终记号列表转换回文本形式时，# 的字符记号（类别码 6）会得到特殊处理：它会输出为 *两个连续字符*: `##`，从而生成下面传递给 Lua 的代码：

`local tbl = {} tbl[1] = "Hello" tbl[2] = "World" print(##tbl)`

在转换为 Lua 代码时，原始的 `#` 已经加倍，这就产生了一个错误：

`\directlua]:1: attempt to get length of a number value`

这个问题源于 TeX 的语法，它使用双井号符号 `##` 来表示或生成单个 `#` 记号；这种语法用于定义其他带参数宏的宏，或者用于为 `\halign` 或 `\valign` 表构造命令创建模板的宏中。这有些令人困惑，所以让我们看一个例子。

#### 示例

假设我们定义一个宏 `\mymacro` 它接收一个参数， `#1`，但它还定义了第二个宏 `\foo` 而该宏本身也接收一个参数。为了区分参数 `#1` 用于 `\mymacro` 以及需要定义 `\foo` 以使用它自己的参数 `#1` TeX 语法要求你使用 `##1` 在……内部 `\mymacro` 来表示与……一起使用的参数 `\foo`:

`\def\mymacro#1{\def\foo##1{#1 Hello##1}}`

如果你写 `\mymacro{Hey!}` 它将把该宏定义为 `\foo` 为

`\def\foo#1{Hey! Hello#1}`

请注意， `\mymacro`的参数 `#1` (`Hey!`）已被纳入 `\foo` 的定义中，而且序列 `##1` 已被转换为 `#1` 在……的定义中 `\foo`。因此我们可以使用 `\foo` 如下：

`\foo{, World!}`

会排版出 `Hey! Hello, World!`

我们可以解决 `\directlua`对……的处理 `#` 字符问题，方法是在 LuaTeX 处理代码之前临时更改它的类别码。例如：

```
\begingroup
   \catcode`\#=11
   \directlua{
   local tbl = {}
   tbl[1] = "Hello"
   tbl[2] = "World"
   tex.print("Table length is "..#tbl)
}
\endgroup
```

这会生成 Lua 代码

```
local tbl = {} tbl[1] = "Hello" tbl[2] = "World" tex.print("Table length is "..#tbl)
```

这样就会排版出我们期望的结果：

`Table length is 2`

### 使用 % 字符

在 TeX/LaTeX 中， `%` 字符通常用于在代码中加入单行注释：向 TeX 引擎发出信号，让它忽略从该位置起直到包含该 `%` 的那一行行尾为止的所有内容。然而，在 Lua 语言中， `%` 字符会用于一些非常有用的字符串处理函数中，例如 `string.format(...)`, `string.gmatch(...)`，以及 `string.gsub(...)` 在这些函数中， `%` 字符作为这些函数语法的一部分，发挥着重要作用。

当与 TeX/LaTeX 一起使用时， `%` 会充当注释字符，因为它被分配了类别码 14。要让它表现为普通字符，并关闭其通常的 TeX/LaTeX 行为，我们需要把它的类别码改成某个安全值，例如 12。下面的 `\directlua` 示例使用了本文前面讨论过的若干技巧，以及一个我们尚未提到的技巧： ``\catcode`\^^M=12``，这使我们能够在代码中使用 Lua 注释；下文会对此进行讨论。

#### 示例

下面的示例借鉴自 [lua-users.org](http://lua-users.org/wiki/StringLibraryTutorial)，并已做适当修改，以便用于 `\directlua`.

```
\documentclass{article}
\begin{document}
\begingroup
\ttfamily
\let\\\relax
\catcode`\^^M=12 %<---we further explore this below!
\catcode`\%=12
\directlua{
   local str -- declare a local variable to hold the result

   tex.print("Using string.format():".."\\par")

   str=string.format("%s %q", "Hello", "Lua user!") -- string and quoted string
   tex.print(str.."\\par")
   str = string.format("%c%c%c", 76, 117, 97) -- char
   tex.print(str.."\\par")
   str=string.format("%e, %E", math.pi, math.pi) -- exponent
   tex.print(str.."\\par")
   str=string.format("%f", math.pi) -- float
   tex.print(str.."\\par")
   str=string.format("%g, %g", math.pi, 10^9) -- float or exponent
   tex.print(str.."\\par")
   str = string.format("%o, %x, %X", 99, 125, 125)  -- octal, hexadecimal, hexadecimal
   tex.print(str.."\\par")

   tex.print("\\vskip3mm".."Using string.gmatch():".."\\par")

   for word in string.gmatch("Hello TeX user", "%a+") do
      tex.print(word.."\\par")
   end

   tex.print("\\vskip3mm".."Using string.gsub():".."\\par")
   str=string.gsub("banana", "(an)", "%1-") -- capture any occurrences of "an" and replace
   tex.print(str.."\\par")
}
\endgroup
\end{document}
```

下方截图展示了上述代码排版后的结果：

![在 \directlua 中使用 Lua 字符串函数](/files/c46ed26f158c7b537c1778ce88b92d9cba38aab6)

## 为什么 Lua 代码显示为单行？

正如你可能注意到的，本文示例中显示的所有（生成的）Lua 代码片段都以单行文本呈现：原本存在于 `\directlua` 代码片段中的换行没有被保留。为什么会这样？因为 Lua 代码中的换行已经被 *去掉了* 在 LuaTeX 于……中的预处理期间 `\directlua`，从而使 Lua 代码变成了一长行文本。该行为可追溯到 TeX 引擎处理行尾字符的方式——它们用 `\r` （回车）和 `\n` （换行）来表示，编程文献中通常如此。至于为什么我们需要关心这些细节，在讨论如何使用 Lua 的机制注释掉代码段时就会清楚。

当软件写入（保存）一个文本文件时，每一行文本都会以所谓的“换行”字符结尾——实际使用的换行字符取决于写出该文件所用的应用程序和操作系统。维基百科有一篇 [有趣的文章](https://en.wikipedia.org/wiki/Newline) 探讨了当今所用换行字符的历史/演变。

对于任意文本文件，其中各行文本可能以不同的字符组合结束，这些字符称为回车（ASCII/Unicode 字符 13）和/或换行（ASCII/Unicode 字符 10），分别记作 `\r` 和 `\n` 分别表示。由于 TeX 引擎被设计为跨平台独立工作，它们需要一种方法来规避文本文件中换行符固有的平台相关性。自然地，TeX 引擎内置了（但可配置的）处理行尾字符的方法。

### TeX 引擎如何处理行尾

当 LuaTeX 处理 `\directlua{⟨code⟩}` 它会读取你在……中包含的文本 `⟨code⟩` 并应用标准 TeX 引擎方法来处理你在……中包含的任何行尾 `⟨code⟩`。默认情况下，这些标准 TeX 方法会将所有行尾字符（回车和换行）移除，并替换为空格字符。我们说“默认”是因为 TeX 引擎对行尾字符的处理可以通过一个用户可配置参数进行修改，该参数称为 `\endlinechar`。这里我们先给出一个简短的两步概述，但更详细的内容可参见 Overleaf 文章 [《\endlinechar 简介：TeX 如何从文本文件读取各行》](/latex/zh-cn/shen-du-wen-zhang/05-an-introduction-to-endlinechar-how-tex-reads-lines-from-text-files.md).

#### 步骤 1：TeX 插入自己的行尾字符

从输入文件读取一行文本后，TeX 引擎会立即移除该行末尾的任何 `\r` 或 `\n` 字符。接着，TeX 引擎会 *插入* （重新添加）它们自己的行尾字符到该行末尾。该字符由一个用户可配置的 TeX 参数决定，该参数称为 `\endlinechar` ，正是通过这种机制，TeX 引擎才能以平台无关的方式处理行尾字符：它们自行选择并设置行尾字符，而不管输入文本文件中原本包含什么。

通常，TeX 引擎使用设置

`\endlinechar=13`

这对应于回车字符（`\r`）。不过，用户始终可以为……赋予另一个字符代码， `\endlinechar`这一点我们将在本文后面看到。

因此，你在……中包含的任何行尾字符 `⟨code⟩` 被……处理时 `\directlua{⟨code⟩}` 都会被移除，并替换为由 TeX 引擎自身决定的单个字符。请注意，TeX 引擎会在从文件中读入新的一行文本之后立刻执行这种行尾处理，而在 *在* 处理该行中的任何字符（以生成记号）之前就这样做。不过，这还不是全部：TeX 引擎对这些行尾字符 *确实* （它已经插入的）处理方式，解释了为什么 Lua 代码会变成单行。

#### 步骤 2：TeX 将自己的行尾字符转换为空格

除了插入由……值定义的自己的行尾字符之外 `\endlinechar`，TeX 引擎还会将类别码 5 用于那些应该被 *视为* 行尾字符的字符。这样，TeX 引擎通常会处理：

1. 由……定义的行尾字符 `\endlinechar`;
2. 同一个字符 *通常* 被赋予类别码 5。

正是 TeX 对该行尾字符的处理，解释了我们关于 Lua 代码单行显示的困惑。当 TeX 引擎处理一行输入时，最终会检测到该行的最后一个字符：由……定义的字符 `\endlinechar`。通常，该字符的类别码为 5，这会使 TeX *将它替换* 为空格字符：也就是说，在行尾，TeX 实际上会去掉其行尾字符并把它换成空格。顺带一提，TeX 引擎也使用类别码 5 的字符来检测空行并开始新段落，但这里我们不讨论这一点。

当然，既然是 TeX，你可以通过重新设置 `\endlinechar` 为其他字符，和/或将分配给 `\endlinechar` 的字符赋予你选择的类别码值，来实现各种特殊的宏编程技巧。

如果你想防止 Lua 代码变成单行文本，你可以（临时）更改分配给 `\endlinechar` 或者更改标准行尾终止符的类别码 `\r`.

### TeX 奇特的 ^^ 记号

在接下来的几节中，我们会遇到 TeX 不常见的 `^^` 记号法，它被称为“扩展字符机制”。它是 Knuth 设计的一种便于输入“控制字符”（如行尾终止符、制表符等）的方法。例如：

* `^^J` 表示字符代码 10（`\n`，换行）；
* `^^M` 表示字符代码 13（`\r`，回车）。

像 `^^M` 这样的字符序列，会在 TeX 的输入扫描过程早期就转换为其对应的字符代码，也就是 TeX 读取输入字符并生成相应字符记号的时候。

### 更改分配给 \endlinechar 的字符

要记住，我们仍然需要防止 `~` 字符展开，我们可以写

```
\begingroup
\endlinechar=10 % Change the end-of-line character to \n
\directlua{
   local x=3
   if x \noexpand~= 4 then
   print("x is not equal to 4")
   end
}% don’t want the \n appearing here
\endgroup% or a \n here
```

上面对……的设置 `\endlinechar` 会使 LuaTeX 追加字符代码 10（`\n`，换行）到它读入的每一行末尾。我们这样做是因为 `\n` （换行）通常具有类别码 12，你可以通过输入 ``\the\catcode`\^^J``. 因为 `\n` 没有类别码 5，LuaTeX 就不会把它转换为空格字符，因此它会保留在 LuaTeX 读入的每一行末尾。这样就会有一个代码为 10 的字符保留在每一行末尾，从而进入由……构建的记号列表中 `\directlua` 并在记号列表转换为文本后重新出现在 Lua 代码中。在上述更改后，Lua 代码会以下列字符序列传递给 Lua 解释器：

**\n**local x=3\*\*\n**if x \~= 4 then**\n**print("x is not equal to 4")**\n**end**\n\*\*

其中 **\n** 记号旨在表示字符代码 10 *不* 某个未知宏 `\n`。现在，Lua 解释器会在代码中看到换行，正如它最初写在……中一样 `\directlua` 命令：

```
   local x=3
   if x ~= 4 then
   print("x is not equal to 4")
   end
```

顺便提一下，请注意 Lua 代码字符串中的第一个字符是 `\n` （在 `本地` 关键字之前）。 `\n` 这

`\directlua{`

来自那一行 `{` 之后立刻有一个换行

`，而这也会被保留。要防止这一点，你可以写`

### 更改 \r 的类别码

为了在 Lua 代码中保留换行，我们还可以更改 `\r` 的类别码为非 5 的值，这样 `\r` 就不再被识别为（被视为）行尾字符。使用这种技术时，LuaTeX 仍然使用 `\endlinechar=13` 并且仍会继续添加一个 `\r` 到每一行末尾；不过，由于 `\r` 不再具有类别码 5，LuaTeX 将不会把该 `\r` 字符识别为行尾字符：它不会把它转换为空格，而会毫发无损地传递过去，使其出现在 Lua 代码中。

要记住，我们仍然需要防止 `~` 字符展开，我们可以写

```
\begingroup
\catcode`\^^M=12 % change category code of \r to 12
\directlua{
   local x=3
   if x \noexpand~= 4 then
   print("x is not equal to 4")
   end
}
\endgroup
```

在这种情况下，Lua 代码会以下列形式传递给 Lua 解释器：

**\r**local x=3\*\*\r**if x \~= 4 then**\r**print("x is not equal to 4")**\r**end**\r\*\*

其中 `\r` 记号旨在表示字符代码 13，而不是某个未知宏 `\r`。与 `\endlinechar` 示例一样，Lua 解释器现在会在代码中看到换行，正如它最初写在……中一样 `\directlua` 命令：

```
   local x=3
   if x ~= 4 then
   print("x is not equal to 4")
   end
```

顺便再次注意，Lua 代码字符串中的第一个字符是 `\r` （在 local 关键字之前）：这也来自那一行

`\directlua{`

#### 为什么 \r 使用类别码 12 而不是类别码 11？

答案是为了避免意外引入由……触发的错误 `\r` （类别码 11）被添加到从输入文件读取的 TeX/LaTeX 命令末尾。看这个例子：

```
\begingroup
\catcode`\^^M=11 % change category code of \r to 11
\directlua{
   local x=3
   if x \noexpand~= 4 then
   print("x is not equal to 4")
   end
}
\endgroup
```

这会产生一个错误：

```
   ! Undefined control sequence.
   l.9 \endgroup
```

怎么会这样呢，因为 `\endgroup` 是一个标准的 TeX 原始命令？这个错误的原因相当微妙：当 LuaTeX 读取最后一行文本——即包含 `\endgroup`——的那一行时，它也添加了 `\endlinechar` @ `\r` 到该行末尾。现在，在其内部内存中，LuaTeX 看到的字符序列是

`\endgroup\r`

其中我们使用 `\r` 来表示代码为 13 的字符，而不是某个未知 TeX 宏的名称 `\r`.

当 LuaTeX 从我们的文本文件读取这一行时，原始的 `\begingroup` 仍然有效：我们正处于一个尚未通过执行相应的 `\endgroup` 命令来关闭的组中——而执行该命令会导致 `\r` 恢复为先前的类别码值 5。

当 LuaTeX 开始从文本行 `\endgroup\r` 进行处理（创建记号）时，它会识别第一个字符 `\` 把它当作转义字符，这会触发 LuaTeX 开始查找命令名。为了识别命令名，LuaTeX 会寻找类别码为 11 的字符序列，但由于 `\r` 也具有类别码 11，LuaTeX 便认为 `\r` 该字符（仍为类别码 11）构成 *命令的一部分* 名为 `\endgroup\r` ，当然这并不存在，所以 LuaTeX 报告一个 `未定义的控制序列` 错误。这就是我们使用类别码 12 而不是 11 的原因。

由于 LuaTeX 的错误消息写到了控制台，我们不容易看到/注意到那个 `\r` 字符，因此不明显知道是什么导致了错误。

### 我们为什么要关注行尾？

原因是为了在代码中启用 Lua 的注释方法！你可以使用 LuaTeX 的标准机制添加 `%` 字符来注释掉代码中的单行；不过，Lua 语言有自己非常有用的 *多行* 注释机制，你可能会想利用它们。

让我们先看看，如果不处理换行问题就尝试使用 Lua 的单行注释，会发生什么。TeX 使用 `%` 字符来注释单行代码，而 Lua 使用双连字符： `--`.

如果我们尝试运行这个会怎样：

```
\directlua{
   local x=3
   if x \noexpand~= 4 then
   -- I'm going to output the result of this complex test
   print("x is not equal to 4")
   end
}
```

我们会得到一个错误：

`[\directlua]:1: 'end' expected near <eof>`

这个错误是由于传递给解释器的 Lua 代码中没有换行造成的，解释器只看到一个连续字符串，而注释是从该字符串中途开始的：

```

local x=3 if x ~= 4 then -- I'm going to output the result of this complex test print("x is not equal to 4") end
```

之后的所有内容 `**local x=3 if x ~= 4 then**` 都被视为被注释掉的内容，这会导致解释器看到一段不完整的 Lua 代码，从而产生错误

`'end' expected near <eof>`.

其中 `<eof>` 表示文件结束。

正如你大概已经猜到的，我们必须通过确保换行被传递到最终生成的 Lua 代码中来解决这个问题，例如，我们可以通过更改 `\r` 的类别码为 12：

```
\begingroup
\catcode`\^^M=12 % change category code of \r to 12
\directlua{
   local x=3
   if x \noexpand~= 4 then
   -- 我要输出这个复杂测试的结果
   print("x is not equal to 4")
   end
}
\endgroup
```

现在，Lua 解释器看到的是一个字符串，但它包含 `\r` 按原样写入的换行符 `\directlua` 片段：

**\r**local x=3\*\*\r**if x \~= 4 then**\r\*\*-- I'm going to output the result of this complex test\*\*\r**tex.print("x is not equal to 4")**\r**end**\r\*\*

这实际上等同于写成

```
   local x=3
   if x \noexpand~= 4 then
   -- 我要输出这个复杂测试的结果
   print("x is not equal to 4")
   end
```

这意味着 Lua 能够正确处理这段代码，并忽略我们注释掉的那一行。

**块注释**

Lua 语言还支持一种它称作 [“块注释”](https://www.lua.org/pil/1.3.html) （或者 *长注释*）：它们以 `--[[` 开始，并一直有效，直到对应的 `]]`。我们可以使用这种方便的语法来编写多行注释，或者注释掉我们想暂时移除的代码段：

```
\begingroup
\catcode`\^^M=12 % change category code of \r to 12
\directlua{
   local x=3
   if x \noexpand~= 4 then
   --[[ I’m going to output the result of this complex test
   simply because it really is
   such an amazing conclusion]]
   print("x is not equal to 4")
   end
}
\endgroup
```

## 总而言之

首先，如果你已经读完了这篇相当长的文章，恭喜你！我们一直努力撰写一份较为全面的 TeX 相关概念和主题指南，以提供通过 `\directlua` command. 我们希望已经写出了一篇具有启发性、并能为 Overleaf 用户社区乃至更广泛读者带来有用价值的文章。和往常一样，我们很乐意收到反馈，所以请随时 [联系我们](https://www.overleaf.com/contact) 就这篇文章留下评论，或提出你希望我们撰写的其他主题建议。

祝 $$\text{Lua}\mathrm{\TeX}\text{-ing!}$$ 来自 Graham Douglas 和 Overleaf 团队。

### 最后……只需使用 luacode 宏包

尽管 TeX 和 Lua 的运行方式根本不同，但这两种语言在各自语境中都共享一些具有“特殊含义”的字符——例如 \\\、%、\~、#、^、&——当然，Lua 和 TeX 赋予这些特殊含义是为了 *非常* 不同的目的。我们对有问题字符的探讨说明了困难为何会出现以及你如何解决它们；不过，手动修复许多小的 Lua 代码片段可能相当繁琐，因此大多数用户更愿意使用 LaTeX 宏包来消除这些挑战。这样一种宏包是 [`luacode`](https://ctan.org/pkg/luacode?lang=en) 它提供了一整套旨在简化处理 `\directlua`，不过，至少你现在应该对这些问题有了更好的理解 `luacode` 为你解决。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ayakaleaf-pro.ayaka.space/latex/zh-cn/shen-du-wen-zhang/09-an-introduction-to-luatex-part-2-understanding-directlua.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
