注意

在您玩 LaTeX 宏时,您会发现它的功能是相当有限的。您可能会想,您每天使用的这些包是如何在如此简单的环境中实现的。实际上,LaTeX 是一组 Plain TeX 宏,大多数包都使用 Plain TeX 代码。Plain TeX 更为底层,具有更多的功能,但代价是学习曲线陡峭且编程复杂。

除了少数例外,您可以在有效的 LaTeX 文档中使用完整的 Plain TeX 语言,而反过来则不行。

术语

为了避免混淆,似乎有必要解释一些术语。

  • 是指从左大括号到匹配的右大括号之间的所有内容。

  • 标记 是字符、控制序列或组。

  • 控制序列 是以 \ 开头的任何内容。它不会按原样打印,而是由 TeX 引擎根据其类型进行扩展。

  • 命令(或函数或宏)是一个控制序列,它可能会扩展为文本、重新定义控制序列等。

  • 原语 是 TeX 引擎硬编码的命令,即它不是写在 Plain TeX 中的。

  • 寄存器 是 TeX 用来处理变量的方式。它们的数量是有限的(在经典 TeX 中,每种类型的寄存器为 256 个,在 e-TeX 中为 32767 个)。

  • 长度 是一个包含长度的控制序列(一个数字后跟单位)。见“长度”。

  • 字体 是指向字体文件的控制序列。见“字体”。

  • 盒子 是用于打印的对象。任何最终打印到纸上的东西都是盒子:字母、段落、页面等。见“盒子”。

  • 胶水 是放置在盒子之间的空间量,当它们被连接时,胶水提供了空间。

  • 计数器 是一个寄存器,包含一个数字。见“计数器”。

可能还有更多术语,但希望这些足够清晰。

Catcodes(类别代码)

在 TeX 中,某些字符具有特殊含义,并且不是直接打印其关联的符号。例如,\ 用于引入控制序列,默认情况下不会打印反斜杠。

为了区分字符的不同含义,TeX 将它们分为类别代码(catcodes)。TeX 中有 16 种类别代码。

TeX 的一个强大功能是它能够重新定义语言本身,因为存在一个 \catcode 函数,允许您更改任何字符的类别代码。

然而,这并不推荐,因为它会使代码难以阅读。如果您在类或样式文件中重新定义了任何类别代码,请确保在文件的末尾将其恢复。

如果您在文档中重新定义了类别代码,请确保在前导部分之后进行,以避免与包的加载发生冲突。

类别代码

代码 描述 默认设置
0 转义字符和控制序列 \
1 组的开始 {
2 组的结束 }
3 数学切换符 $
4 对齐标签 &
5 行结束符 ^^M(ASCII 返回)
6 宏参数 #
7 上标 ^^^K
8 下标 _^^A
9 被忽略的字符 ^^@(ASCII 空字符)
10 空格 ^^I(ASCII 水平制表符)
11 字母 A...Za...z
12 其他字符 所有未列出的字符,尤其是 @
13 活跃字符 ~^^L(ASCII 换页符)
14 注释字符 %
15 无效字符 ^^?(ASCII 删除)

活跃字符

活跃字符类似于宏:它们是单个字符,在任何其他命令之前会被扩展。

\catcode`| = 13
\def|{\TeX}
...
This is a stupid example of |.
This is a stupid example of TeX.

请注意,活跃字符需要直接跟随一个定义,否则编译会失败。

示例

Texinfo
Texinfo 使用类似 TeX 的语法,但有一个主要的区别:所有函数都是以 @ 而不是 \ 开头。这并非偶然:它实际上使用 TeX 来打印文件的 PDF 版本。基本上,它通过输入 texinfo.tex 来重新定义控制序列字符。可能的实现:

\catcode`\@=0
@def@@{@char64} % 写 '@' 字符。
\catcode`\\=13 @def\{{@tt @char92}}

The @TeX 命令以前是写作 '\TeX'。现在写作 '@@TeX'。

此重新定义后,@ 应该会引入每个命令,而 \ 将实际打印反斜杠字符。

项目符号(Itemize)

一些人可能会觉得 LaTeX 列表环境的语法有点繁琐。这里是一个快速定义类似 wiki 的项目符号的方法:

\catcode`| = 13
\def|{\item {--}}
\def\itemize#1{{\leftskip = 40 pt #1 \par}}

\itemize{
| First item
| Second item
}

美元符号和数学

如果您有许多美元符号需要打印,您可以更改数学切换字符。

\catcode`$ = 11
\catcode`| = 3

It costs $100.
Let's do the math: |50+50=100|. Let's highlight it:
||50+50=100||

\makeatletter 和 \makeatother

如果您进行过一些 LaTeX 的黑客操作,您一定遇到过这两个命令,\makeatletter\makeatother

在 TeX 中,@ 字符默认属于 catcode 11(字母)。这意味着您可以将它用于宏名称。LaTeX 利用这个 catcode 来规定一条规则:所有不公开的、内部的宏(即不应被最终用户访问的宏)名称中至少包含一个 @ 字符。在文档中,LaTeX 会将 @ 的 catcode 更改为 12,其他字符则保持不变。

因此,当您需要访问 LaTeX 内部的功能时,必须使用 \makeatletter\makeatother 将所有访问私有功能的命令包裹起来。它们仅仅是更改了 catcode:

\def\makeatletter{\catcode`@ = 11}
\def\makeatother{\catcode`@ = 12}

Plain TeX 宏

\newcommand\renewcommand 是 LaTeX 特定的控制序列。它们会检查是否有现有的命令被新定义的命令所覆盖。

在 Plain TeX 中,宏定义的原语不会检查可能的覆盖情况。您需要自己确保不破坏任何已有的定义。

其语法为:

\def<macroname>#1<sep1>#2<sep2>{宏内容,使用参数 #1 等}

您可以在参数之间使用几乎任何字符序列。例如,下面我们定义一个简单的宏,将小数点分隔符从点改为逗号:

\def\pointtocomma #1.#2{(#1,#2)}

这样,当您写下:

\pointtocomma 123.456

它将打印 (123,4)56。我们加上括号只是为了突出问题。每个参数是匹配宏定义的最短可能输入序列,包括分隔符。因此,#1 匹配所有直到第一个点的字符,#2 仅匹配第一个标记,即第一个字符,因为它后面没有分隔符。

解决方案: 添加第二个分隔符。一个空格可能看起来很方便:

\def\pointtocomma #1.#2 {(#1,#2)}

通常来说,每次您预期有多个参数并且有特定的分隔符时,最好考虑最后一个分隔符。如果您不想处理分隔符,那么 Plain TeX 宏的使用方式与 LaTeX 宏相同(没有默认参数):

\def\mymacro#1#2#3{{\bf #1}#2{\bf #3}}
%% ...
\mymacro{word1}{word2 word3}{!!!}

扩展定义

TeX 还有另一个定义命令:\edef,表示扩展定义。语法与 \newcommand 相同:

\edef<macroname><argumentslist>{<expanded content>}

该内容会在使用 \edef 的地方扩展(但不会执行,即不会打印),而不是在宏被定义的地方。宏扩展并不总是显而易见的...

示例:

\def\intro{Example}
\edef\example#1{\intro~---~#1}
\def\intro{Exercise}

\example{This is an example}

在这里,\intro 的重新定义对 \example 没有影响。

全局定义

定义是有限制的,只在其作用域内有效。然而,有时可能需要在某个组内定义一个宏,使其在该组外依然有效,直到文档结束。这就是所谓的全局定义。

{
\def\LocalTeX{Local\TeX}
\global\def\GlobalTeX{Global\TeX}
}
I can still access the \GlobalTeX{} macro here.

您还可以在 \edef 中使用 \global 命令。

这两个命令有快捷方式:

  • \gdef 等同于 \global\def

  • \xdef 等同于 \global\edef

长定义

前述定义命令不允许在多个段落中使用,即包含 \par 命令或双换行的文本。

您可以在定义前加上 \long 命令,以允许多段落的参数。

示例:

\long\def\dummy#1{#1}
\dummy{First paragraph\par Second paragraph}

外部定义

此前缀宏可防止某些上下文中使用定义。它有助于整合宏并减少由于上下文不当而导致的错误。外部宏的定义用于不依赖于任何上下文的情况,因此得名。

例如,下面的代码会失败:

\outer\def\test{a test}
\def\failure{\test}

外部宏不能出现在以下情况下:

  • 宏参数中

  • 被跳过的条件中

  • ...

  • \let\futurelet

\let<csname><token> 等同于 \expandafter\def\expandafter<csname>\expandafter{<content>}。它定义了一个新的控制序列名称,该名称等于指定的标记。通常,标记是另一个控制序列。

注意,\let 只会扩展一次标记,而与之不同的是,\edef 会递归地扩展,直到不再有扩展为止。

示例:

使用 \let

\def\txt{a}
\def\foo{\txt}
\let\bar\foo
\bar % 打印 a
\def\txt{b}
\bar % 打印 b

使用 \edef

\def\txt{a}
\def\foo{\txt}
\edef\bar{\foo}
\bar % 打印 a
\def\txt{b}
\bar % 打印 a

\futurelet 语法

\futurelet<csname><token1><token2>... 的工作方式稍微不同。首先,token2 被分配给 csname,然后 TeX 处理 <token1><token2>... 序列。因此,\futurelet 允许您在使用时分配一个标记。

特殊控制序列名称
有些宏的名称不能直接写出。这种情况出现在宏的名称由多个宏名称组成的情况下。示例如下:

\def\status{full}
\def\varempty{This is empty}
\def\varfull{This is full}

\csname var\status \endcsname

上面的最后一行将根据 \status 的值打印相应的句子。

此命令实际上执行了与 \string 相反的操作,\string 用于打印控制序列的名称而不进行扩展:

{\tt \string\TeX}

输出:

\TeX

控制扩展
\expandafter{token1}{token2} 将在扩展 token1 之前扩展 token2。有时当 token2 需要扩展但由于 token1 的存在而不能扩展时,这个命令就很有用。

{\tt \expandafter\string\csname TeX\endcsname}

输出:

\TeX

\noexpand 对于精确控制哪些内容在 \edef 中被扩展非常有用。示例如下:

\def\intro{Example}
\def\separator{~---~}
\edef\example#1{\intro\noexpand\separator#1} 

\example{no expand makes the separator dynamic in an {\tt \string\edef}.}
\def\intro{For instance}
\def\separator{~:~}

\example{the separator changed, but not the first word.}

控制序列
\the 控制序列将让你查看各种 TeX 类型的内容:

  • catcodes

  • chardef

  • 字体参数

  • 内部参数

  • 长度

  • 寄存器

  • 等等

示例:

文本尺寸:$ \the\hsize \times \the\vsize $

寄存器
寄存器是类型化的变量。它们的数量有限,范围从 0 到 255,共有 6 种不同类型:

类型 描述
box 一个盒子
count 一个整数
dimen 一个长度
muskip 一种胶水(mu 单位)
skip 一种胶水
toks 一系列标记

TeX 在内部使用一些寄存器,因此最好不要使用它们。

保留寄存器列表:

  • \box255 用于页面内容

  • \count0-\count9 用于页面编号

临时寄存器(可自由使用):

  • \box0-\box254

  • \count255

  • \dimen0-\dimen9

  • \muskip0-\muskip9

  • \skip0-\skip9

使用 = 控制字符来分配寄存器。对于盒子寄存器,使用 \setbox 命令。

\count255=17
\setbox\mybox=\hbox{blah}

您可以使用以下宏来防止冲突:

  • \newbox

  • \newcount

  • \newdimen

  • \newmuskip

  • \newskip

  • \newtoks

这些宏使用以下语法:\new*<csname>,例如:

\newbox\mybox
\setbox\mybox=\hbox{blah}

这些命令不能在宏内使用,否则每次调用宏时都会为其保留另一个寄存器。

您可以使用 \the 命令来打印寄存器。对于计数器,使用 \number 命令。对于盒子,使用 \box 命令。

\the\hsize
\number\count255
\box\mybox

算术运算
TeX 的算术能力非常有限,尽管这个基础足以扩展一些有趣的特性。主要有三个函数:

  • \advance <register> by <number>

  • \multiply <register> by <number>

  • \divide <register> by <number>

register 可以是类型为 countdimenmuskipskip 的寄存器。对于 boxtoks 类型的寄存器,无法进行算术运算。

条件语句
基础语法如下:

\if* <test><true action>\fi
\if* <test><true action>\else<false action>\fi

其中 \if* 是以下命令之一。

控制序列 描述
\if <a><b> 如果两个字符代码相等,返回真。
\ifcat <a><b> 如果两个类别代码相等,返回真。
\ifdim <a><rel><b> 测量关系,可能为 <, >, 或 =
\ifeof 如果是文件结束或文件不存在,返回真。
\iffalse 总是返回假。
\ifhbox <reg> 如果盒子寄存器包含水平盒子,返回真。
\ifhmode 如果在水平模式下,返回真。
\ifinner 如果在内部模式下,返回真。
\ifmmode 如果在数学模式下,返回真。
\ifnum <a><rel><b> 数字关系,可能为 <, >, 或 =
\ifodd <num> 如果数字是奇数,返回真。
\iftrue 总是返回真。
\ifvbox <reg> 如果盒子寄存器包含垂直盒子,返回真。
\ifvmode 如果在垂直模式下,返回真。
\ifvoid <reg> 如果盒子寄存器为空,返回真。
\ifx <a><b> 如果两个宏扩展结果相同,或者两个字符代码相等,或者两个类别代码相等,返回真。

示例:

\ifnum 5>6
This is true
\else
This is false
\fi

输出:

This is false

自定义条件语句
你可以使用 \newif 命令创建新的条件语句(类似布尔变量)。通过这些自定义条件语句,你可以优雅地控制代码的输出。以下是示例:

需要生成两种版本的文档。一种版本给 A 组,另一种给其他人(即不属于 A 组的人):

  1. 使用 \newif 来定义条件(即布尔变量)。

\newif\ifgroupA
  1. 通过如下方式为条件赋值(真或假):

\groupAtrue % 或者
\groupAfalse

即:

\<conditionalsname>true
\<conditionalsname>false

取决于你希望为条件设置的值。

  1. 现在可以在 if 控制结构中使用条件语句。

\ifgroupA
  % 在这里写给 A 组的文档代码
\else
  % 在这里写给其他人群体的文档代码
\fi

完整示例:

\newif\ifdirector

% 设置条件为假
\directorfalse

\ifdirector
 I write something for the director.
\else
 I write something for common people.
\fi

输出:

I write something for common people.

案例语句
语法如下:

\ifcase <number><case0>\or<case1>\or...\else<defaultcase>\fi

如果数字等于某个案例编号,则打印该内容。注意,编号从 0 开始。

\ifcase 2 a\or b\or c\or d\else e\fi

输出:

c

\else 用于指定默认情况(当没有其他案例匹配时)。

循环
基础语法如下:

\loop <content> \if*<condition><true action>\repeat

和以往一样,contenttrue action 是任意的 TeX 内容。\if* 指代任何条件语句。注意没有“假”动作,你不能在 \if*\repeat 之间使用 \else。如果需要,可以通过改变条件或使用 \newif 定义新的条件语句来解决。示例:

\count255 = 1
\loop
  \TeX
\ifnum\count255 < 10
\advance\count255 by 1
\repeat

这段代码将打印 TeX 十次。

什么都不做
有时你可能需要告诉 TeX 你什么都不做。可以使用两个命令:\relax\empty

经典示例:

\def\myspace{\hskip 25pt\relax}
\myspace{} plus 10pt

\relax 防止在命令后遇到加号或减号时产生不期望的行为。

\empty\relax 的区别在于扩展:\empty 在宏扩展后会消失。

TeX 字符
我们可以使用 \char {charcode} 命令打印所有字符。charcode 实际上是字节值。例如:

\char65 = \char `A = \char `\A

大多数字符对应于 ASCII 值(例如 A-Z、a-z),一些字符替代了 ASCII 中的不可打印字符。

chardef 和 mathchardef
你可以定义控制序列,使其展开为特定的字符。语法为 \chardef<控制序列>=<字符代码>。以下序列做了相同的事情:

\chardef\myA=65
\chardef\myA=`A
\chardef\myA=`\A

示例:

\mathchardef\alphachar = "010B
$\alphachar$

字体编码映射
我们可以使用上述原语打印字体编码映射。

\count255 = 0
\loop
  [\number\count255 =\char\number\count255]
\ifnum\count255 < 127
\advance\count255 by 1
\repeat

另一种版本,使用不同字体,每行一个条目:

\count255 = 0
\loop
  [\number\count255 =
    \char\number\count255 \ 
    {\tt \char\number\count255}
    {\it \char\number\count255}
  ]
  \hfil\break
\ifnum\count255 < 127
\advance\count255 by 1
\repeat

Verbatim 行和空格
发现(La)TeX 将所有空格视为相同类型的间距胶水可能会让人困惑。Plain TeX 提供了一些命令来保留你写入的空格和换行符:

\begingroup
\obeylines
\obeyspaces
Relevant text here
\endgroup

这意味着你可能需要将自己的 verbatim 环境和命令组合起来:

\newenvironment{myverbatim}{\begingroup \obeylines \obeyspaces}{\endgroup}
\newcommand{\mycommand}[n]{do something with #1 .. #n}

然后在你的 .tex 文件中:

\begin{myverbatim}
\mycommand{
whichever text it is important you
preserve the spacing and newslines
for, like when you want to generate
a verbatim block later on.
}
\end{myverbatim}

宏定义宏
在某些情况下,这非常有用,例如为了定义语言命令,如在《多语言版本》中所解释的那样,用户可以写:

\en{some english text}
\de{etwas deutscher Text}

并确保它切换到适当的 Babel 语言。

让我们定义一个宏,用来定义语言命令。这些命令很简单:如果参数是 \locale 变量的值,那么相应的宏直接打印它的内容。否则,它什么也不做。

基本上,我们要做的是定义一组这样的宏:

\newcommand{\de}[1]{#1}
\newcommand{\en}[1]{}
\newcommand{\fr}[1]{}

在上述代码片段中,只有 \de 命令会输出它的内容, \en\fr 不会输出任何内容。这正是我们想要的。问题出现在你想自动化任务,或者有很多语言时,你想更改语言选择。你只需要移动 #1,但这并不方便,也无法从命令行选择 Babel 语言。思考一下...

我们将动态定义语言命令,基于 \locale 变量(或任何你选择的变量)。因此,使用 ifthen 包中的 \equal 命令。

由于在 LaTeX 中编写是非常困难的,我们将使用一些 Plain TeX。

\def\locale{de}

\def\localedef#1{
  \ifthenelse{ \equal{\locale}{#1} }{
    %% 设置 Babel 语言。
    %% 定义命令来打印内容。
  }{
    %% 定义命令打印空内容。
  }
}

另一个问题是:如何定义一个名称是变量的命令?在大多数编程语言中这是不可能的。我们可能会想要写:

\def\#1 #1{#1}

这会失败有两个原因:

  1. 最后的两个 #1 应该指向新宏的参数,但它们会先展开为 \localedef 宏的第一个参数,因为它们在该宏的正文中。

  2. \#1 会展开成两个符号:#1,而 \def 命令会失败,因为它需要一个有效的控制序列名。

问题1的解决方案很简单:使用 ##1,它在宏执行时会展开为 #1

对于问题2,解决方法有点棘手。我们可以告诉 TeX 特定的符号是一个控制序列。\csname...\endcsname 就是用来做到这一点的。然而:

\def\csname#1\endcsname ##1{##1}

会失败,因为它会重新定义 \csname#1,这不是我们想要的,然后 TeX 会遇到 \endcsname,从而导致错误。

我们需要延迟 \def 的展开,即告诉 TeX 先展开 \csname 部分,然后应用 \def。有一个命令可以做到这一点:\expandafter{token1}{token2}。它会先展开 {token2},然后再展开 {token1}

最终,如果我们想从命令行设置语言,我们必须能够设置 \locale 变量,使得源代码中的语言成为默认值,可以被命令行中的值覆盖。这可以通过 \providecommand 来实现:

\providecommand\locale{fr}

最终的代码是:

%% 必需的包
\usepackage{ifthen}

%% 生成语言命令的 TeX 函数
\def\localedef#1#2{
  \ifthenelse{ \equal{\locale}{#1} }{
    \selectlanguage{#2}
    \expandafter\def\csname#1\endcsname ##1{##1}
  }{
    \expandafter\def\csname#1\endcsname ##1{}
  }
}

%% 选择的语言,可以放在语言命令之前的任何位置
\providecommand\locale{fr}

%% 语言命令
\localedef{de}{ngerman}
\localedef{en}{english}
\localedef{fr}{frenchb}
%% ...

你可以使用以下命令进行编译:

latex '\providecommand\locale{en}\input{mydocument.tex}'

附录
参考文献和更多阅读:

  • 《The TeXbook》, Donald Knuth

  • 《TeX by Topic》, Victor Eijkhout

  • 《TeX for the Impatient》, Paul W. Abrahams, Karl Berry 和 Kathryn A. Hargreaves

  • 《TeX command reference》在维基书


Last modified: Wednesday, 23 April 2025, 11:50 AM