> For the complete documentation index, see [llms.txt](https://overleaf-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://overleaf-pro.ayaka.space/latex/zh-cn/shen-ru-wen-zhang/09-an-introduction-to-luatex-part-2-understanding-directlua.md).

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

## 本文的目标

在本文的第一部分中， [LuaTeX 入门（第 1 部分）：它是什么——又是什么让它如此不同？](/latex/zh-cn/shen-ru-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` 充分发挥 LuaTeX 的能力，需要具备若干 TeX 主题的背景知识：TeX 的记号、记号列表和展开机制。本文的目标是探索并解释这些基础 TeX 概念：梳理 `\directlua` 背后的 TeX 相关流程，以帮助你理解它的工作方式，并为你使用 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` 命令允许用户将 Lua 代码嵌入 TeX 文档；这些代码随后会被传递给 LuaTeX 内置的 Lua 语言解释器。然而， `\directlua` 还允许你 *把* Lua 和（La）TeX 代码一起 `\directlua` 放在同一个 \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{⟨代码⟩}`。 `⟨代码⟩` 会被完全展开，然后送入 Lua 解释器。对 `⟨代码⟩`进行读取和展开之后，得到的记号列表会被转换成字符串，就像使用 `\the\toks`.

显示出来一样。当然，从技术上说这是准确的，但如果没有一些低层 TeX 过程——比如记号和展开——的知识，也许并不容易理解。

## 理解 \directlua：我们要讨论哪些主题？

在本文中，我们将更深入地审视一些关键背景主题，并提供若干示例，旨在演示 `\directlua` 如何工作，以及在文档中混合 TeX 和 Lua 时何处（或为何）需要小心。 `⟨代码⟩`.

我们将足够详细地探讨以下主题，以为理解 `\directlua` 及其对你在其中使用的代码所做的“预处理”打下基础：

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

如果你理解 TeX 引擎如何创建并使用记号，以及对 TeX 的展开机制有所认识，那么你就具备了解锁 LuaTeX `\directlua` 命令。

## 基础：从文本到记号，再从记号到文本

Overleaf 已发表过几篇深入探讨 TeX 记号及相关概念的文章，因此这里我们不会重复所有材料；相反，我们将概述与更好理解 `\directlua`.

相关的那些领域/主题：

* [什么是 TeX 记号？](/latex/zh-cn/shen-ru-wen-zhang/53-what-is-a-tex-token.md)
* [什么是 TeX 记号列表？](/latex/zh-cn/shen-ru-wen-zhang/54-what-is-a-tex-token-list.md)
* [\expandafter 是如何工作的：TeX 记号入门](/latex/zh-cn/shen-ru-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 引擎如何计算字符记号？

#### 首先，我们需要记住，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})$$

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

$$\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 为字符附加“特殊含义”，使其成为所谓的 *活动字符* ，其行为就像一个迷你宏：不需要前导 `\` ，孤立的该字符由于其类别码就足以触发其类似宏的行为。

由于活动字符充当迷你宏，它不会被转换为一个 *。通过计算这个会传递给 TeX 内部处理的记号值，该特定* ，而是转换为第二种（整数）记号类型，称为一个 *命令记号*。其计算如下：

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

1. 计算一个中间值，称为 $$\text{curcs}$$ (**当**前 **c**控 **制**序列）其中 $$\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（转义字符）的字符都可以用来代替。 *控制词* 和 *控制符号*:

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

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

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

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

8 位引擎用来创建命令记号整数的计算方法可在这篇 [Overleaf 文章](/latex/zh-cn/shen-ru-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$$ 为

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

记号（整数）是 TeX 引擎“封装”关于输入项（字符或命令）所需记录的一切信息的机制。然而，有时 TeX 引擎需要逆转记号化过程——找出最初读入了什么，才生成那个记号值——即一个单独字符，或构成命令名称的一个或多个字符序列：

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

其中 $$\text{curcs}$$ 是根据所生成的命令记号类型计算的：活动字符、控制符号或控制词。 $$\text{curcs}$$ 变量是一个 *极其* 重要组成部分 $$\text{curcs}$$ 从该命令记号中提取 $$\text{curcs} = \text{token value} - (2^{29} -1)$$.

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

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

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

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

* 把错误或警告信息写到屏幕上或 `.log` 文件；
* 通过 `\write` 命令更改；
* 把 TeX/LaTeX 代码输出到文本文件时，在 `\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`——诸如 `\newcommand` 这样的 LaTeX 命令就是围绕低层原语构建附加功能的宏，这些原语最终会执行实际的宏定义过程。宏可以看作赋予某个已存储记号列表的名称：一个记号列表。

TeX 引擎 *广泛* 使用记号列表，尤其是 [临时的内部专用列表](/latex/zh-cn/shen-ru-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-ru-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` 的通常用法写成 `\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{⟨代码⟩}` 可以要求处理 `⟨代码⟩` 其中既包含 Lua 代码也包含 TeX/LaTeX 代码，但 LuaTeX 内置的 Lua 语言解释器并不理解 TeX 或 LaTeX：那么这怎么能工作呢？如何可能让 `⟨代码⟩` 包含 TeX/LaTeX 指令，而又不会让 Lua 解释器被它不理解的命令彻底搞糊涂呢？例如，下面的 `\directlua` 命令只使用 TeX 宏，但它可以工作：

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

这 `\directlua` 命令会使 LuaTeX 排版 `Hello` 但为什么以及怎样会这样，因为 Lua 语言并不理解 TeX 宏？

答案包含在我们从 [LuaTeX 参考手册](http://www.pragma-ade.com/general/manuals/luatex.pdf) 借用的前面那段描述中，但我们可以把它理解为 `\directlua{⟨代码⟩}` 通过 LuaTeX 最初对 `⟨代码⟩` 进行“预处理”来实现，然后才把任何内容交给 Lua 解释器。这个“预处理”的性质——也就是它真正意味着什么，以及它对你的 `⟨代码⟩`的后果——是我们接下来要讨论的主题，以帮助有兴趣的读者利用 `\directlua`.

### LuaTeX 如何处理 \directlua：初看

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

![](/files/99cab92cbbe3ee2c84a279399636e4bd6bd9161d)

但是等等，把文本变成记号，又立刻把这些记号转换回文本，这样做有什么意义呢？你可能不会惊讶地发现，是的，这张图里还有一个我们没有包含进去的额外且关键的过程： *记号展开*。由你的 `⟨代码⟩` 中的文本生成的每个记号 *会经过一种“检查”，LuaTeX 会对该记号进行测试，看它是否表示属于名为*的子集中的某个命令。 *移除* 如果是，LuaTeX 就会通过 `⟨代码⟩` 和 *把该命令从你的* 中滤掉， *记号展开*.

### 用 TeX 引擎称为

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-ru-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()` 创建的记号流被构建成一个很长的记号列表——该列表中的记号包括通过对你的 `⟨代码⟩`中存在的可展开命令（如宏）应用展开而生成的记号。另请注意， `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 预处理活动的核心：它的主要任务是展开你 `⟨代码⟩` 文本中包含的所有可展开 TeX/LaTeX 命令，并把它处理过的所有内容构造成一个记号列表。再次强调， `scan_toks()` *不会执行不可展开命令* （记号）：它只是 *存储* 这些记号到它正在构建的记号列表中。一旦完成，该记号列表随后会被转换 *回文本表示* 通过 `tokenlist_to_cstring()`——记号列表是一个纯 TeX 的概念，对 Lua 解释器来说完全陌生，因此需要把它转换为文本，变成可传给 Lua 解释器的 Lua 代码。

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

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

```
\count75=1564 % “TeX 世界”中存在的数据
\directlua{
   local x=\number\count75 \space % 将 TeX 数据传输到“Lua 世界”
   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` 正在构建的主记号列表中，随后在记号列表转换为文本时又被转换回文本表示。从 LuaTeX 内部存储的寄存器值到供 Lua 代码使用的数字，这无疑是一条非常迂回的路径，但归根结底它确实有效。 `\count75` 。

**提示：** 如果你想检查 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` 文件的内容写出到 `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{⟨代码⟩}` 会执行 *完全展开* 你的 `⟨代码⟩`的 `\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` 文件

`中产生如下文本`

这表明 `\mydollar` 是 *不* 在 `\directlua`的预处理中被展开。 `\mydollar` 出现在

之后的空格是命令记号转换为其文本表示时添加的。 `\chardef` 用 *不可展开的* 创建控制序列时，TeX 对该控制序列（命令）的内部分类会使其成为 `\directlua` 这与用宏定义命令之一：\def、\edef、\gdef 或 \xdef 定义的控制序列相比，行为截然不同。如上所述，在构建其记号列表的过程中

**关于 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` 已 *会经过一种“检查”，LuaTeX 会对该记号进行测试，看它是否表示属于名为* 和 都很好地说明了把 TeX 引擎的展开过程看作执行“记号操作”：这里的操作就是阻止一个或多个后续记号（命令）展开。因为 `\noexpand` 和 `\unexpanded` 二者都是可展开命令，所以它们会在 `\directlua`的预处理过程中被移除并处理（执行），同时它会从你的 `⟨代码⟩`.

#### \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 引擎执行展开时，任何可展开命令都会 *从 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 宏定义

该 `\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 语法中没有意义，因此会导致错误 `在 '\\' 附近出现意外符号`.

**趣闻**：The `\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{⟨代码⟩}` 会执行 *完全展开* 的一个记号值 `⟨代码⟩`：移除所有可展开命令，并用其展开行为的结果替代它们——继续对 *进一步展开* 可展开命令初次展开所产生的任何记号。

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

**示例**

假设我们定义宏 `\mymacro` 的通常用法写成 `\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 会尝试构造该字符串，但会失败并产生错误：

`在 ' "T\k' 附近存在无效的转义序列。`

在本文后面我们会解释“invalid escape sequence”的含义。

现在让我们对比使用 `\mymacro` 与将 `\TeX` 记号放入由 `\\toks` 命令设置为特定值：

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

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

`local x = "\TeX "`

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

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

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

`在 ' "\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` 未定义，它会导致错误：

```
   ！未定义的控制序列。
   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` 是一个或多个十六进制数字组成的序列，表示该字符的码点。请注意，包围用的括号 `{ }` 是必需的。

### 控制转义序列

传统上，字符串使用双引号定义，例如 `"this is a string"`；在这样的字符串中你可以使用转义序列： `"this is a string.\nI'll now start on a new line."`。然而，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 排版 `Hello, World!`

该 `*string*` 用于 `tex.print(*string*)` 中也可以包含表示 TeX 和 LaTeX 命令的文本，供 LuaTeX 处理。然而，TeX/LaTeX 命令以一个 `\` 字符开头，这在使用双引号创建的字符串中会有问题，因为 Lua 会尝试解析该字符串，检测到开头的 `\` 字符并将其解释为转义序列的开始。当 Lua 尝试处理该转义序列时，通常会失败，因为开头的 `\` 与许多 TeX/LaTeX 命令名称中的第一个字符组合后，并不会构成 Lua 所知的有效转义序列。例如，在处理如下字符串时 `"I like \LaTeX"` Lua 会看到 `\L` 并报错“无效的转义序列”，这就是上面所述错误的原因。

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

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

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

`**\protected**\def\newtest#1{参数：#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;！
```

在纯 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⟩` 字符，但其余文本会原样输出，而 `{` 和 `}` 会被视为一个组而不被打印。

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

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

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

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

![](/files/784bdd210504ac8abe2fdd23d827f845a997be35)

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

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

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

在 `\directlua{⟨代码⟩}`的情况下，如果你的 `⟨代码⟩` 完全展开版本产生了，或者包含不可展开的 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 % 将 \count100 设为 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()`的 LuaTeX Lua API 函数，从而避免使用 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（**一个**pplication **P**rogramming **I**nterface），它们提供了工具，使你能够通过使用 Lua 作为驱动来控制 LuaTeX 的排版行为，从而构建复杂的排版与文档工程解决方案。

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

## 一些示例和陷阱

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

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

Lua 语言使用 `~` 字符（称为波浪号）作为其语法的一部分，包括用于执行“非等于”测试的语法；例如，要测试一个变量 `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`

该 `~` 字符已经被 *从 TeX 当前输入源中移除，* 和 *展开* 成其组成命令——上面的 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 不等于 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 排版出

`表长度是 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)
```

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

`表长度是 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 -- 声明一个局部变量来保存结果

   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 的机制来注释掉代码段时就会变得清楚。

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

对于任意文本文件，其中各行文本的结尾可能由各种字符组合终止，这些字符称为回车（ASCII/Unicode 字符 13）和/或换行（ASCII/Unicode 字符 10），分别记作 `\r` 和 `\n` 。由于 TeX 引擎被设计为平台无关，因此它们需要一种方法来规避文本文件中行尾本质上依赖平台的特性。自然地，TeX 引擎内置了一种（但可配置的）处理行终止字符的方法。

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

当 LuaTeX 处理 `\directlua{⟨代码⟩}` 时，它会读取其中的文本 `⟨代码⟩` 并应用标准 TeX 引擎方法来处理其中包含的任何行尾 `⟨代码⟩`。默认情况下，这些标准 TeX 方法会移除所有行终止字符（回车和换行），并用空格字符替代。我们说“默认情况下”，是因为 TeX 引擎对行终止字符的处理可以通过一个用户可配置的参数来修改，该参数称为 `\endlinechar`。这里我们将给出一个简短的两步概述，但更多细节可见于 Overleaf 文章 [《\endlinechar 简介：TeX 如何从文本文件读取行》](/latex/zh-cn/shen-ru-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`——我们稍后会在本文中看到。

因此，你的 `⟨代码⟩` 中待处理的任何行终止字符都会被 `\directlua{⟨代码⟩}` 移除，并替换为由 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 % 将行尾字符改为 \n
\directlua{
   local x=3
   if x \noexpand~= 4 then
   print("x is not equal to 4")
   end
}% 不希望 \n 出现在这里
\endgroup% 或这里也不要有 \n
```

上面对 `\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{`

，因为在开头的 `{` 后面立即有一个换行，而这也会被保留。要避免这一点，你可以写

`\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
```

这会产生一个错误：

```
   ！未定义的控制序列。
   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\*\*-- 我要输出这个复杂测试的结果\*\*\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
   --[[ 我要输出这个复杂测试的结果
   仅仅因为它确实是
   如此惊人的结论]]
   print("x is not equal to 4")
   end
}
\endgroup
```

## 总之

首先，如果你已经读完了这篇相当长的文章，恭喜你！我们试图撰写一份相当全面的 TeX 相关概念与主题指南，这些内容提供了充分利用 LuaTeX 所需的背景，以便通过 `\directlua` 命令。我们希望这篇文章既具启发性，又能为 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://overleaf-pro.ayaka.space/latex/zh-cn/shen-ru-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.
