LaTeX/Plain TeX
当你使用 LaTeX 宏时,你会发现它非常有限。你可能会好奇,你每天使用的所有这些软件包是如何用这么少的代码实现的。事实上,LaTeX 是一套 Plain TeX 宏,大多数软件包都使用 Plain TeX 代码。Plain TeX 的级别要低得多,它有更多功能,但学习曲线陡峭,编程复杂。
除了少数例外,你可以在有效的 LaTeX 文档中使用完整的 Plain TeX 语言,反之则不然。
词汇
[edit | edit source]为了避免混淆,有必要解释一些术语。
- 一个 组 是在开括号之后和匹配的闭括号之前的任何内容。
- 一个 标记 是一个字符、一个控制序列或一个组。
- 一个 控制序列 是任何以
\
开头的内容。它不会按原样打印,而是根据其类型由 TeX 引擎进行扩展。 - 一个 命令 (或 函数 或 宏)是一个控制序列,它可能扩展为文本,控制序列的(重新)定义等。
- 一个 原语 是一个在 TeX 引擎中硬编码的命令,即 它不是用 Plain TeX 编写的。
- 一个 寄存器 是 TeX 处理变量的方式。它们的数量是有限的(在经典 TeX 中,每种类型的寄存器有 256 个,在 e-TeX 中有 32767 个)。
- 一个 长度 是一个包含长度的控制序列(一个数字后跟一个单位)。参见 长度。
- 一个 字体 是一个引用字体文件的控制序列。参见 字体。
- 一个 盒子 是一个用于打印的对象。出现在纸张上的任何内容都是一个盒子:字母、段落、页面......参见 盒子。
- 一个 胶水 是一个特定的空间量,当盒子被连接在一起时,它被放置在盒子之间。
- 一个 计数器 是一个包含数字的寄存器。参见 计数器。
可能还有更多的术语,但我们希望现在已经足够了。
类别码
[edit | edit source]在 TeX 中,一些字符具有特殊的含义,而不是打印相关的字形。例如,\
用于引入控制序列,默认情况下不会打印反斜杠。
为了区分字符的不同含义,TeX 将它们分成 类别码,简称 类别码。TeX 中有 16 种类别码。
TeX 的一项强大功能是它能够重新定义语言本身,因为有一个 \catcode
函数,它可以让你更改任何字符的类别码。
然而,不建议这样做,因为它会使代码难以阅读。如果你在一个类或样式文件中重新定义了任何类别码,请确保在文件末尾将其恢复。
如果你在文档中重新定义了类别码,请确保在序言之后进行,以防止与软件包加载冲突。
代码 | 描述 | 默认集 |
---|---|---|
0 | 转义字符和控制序列 | \
|
1 | 组的开始 | {
|
2 | 组的结束 | }
|
3 | 数学转移 | $
|
4 | 对齐制表符 | &
|
5 | 行尾 | ^^M (ASCII 回车) |
6 | 宏参数 | #
|
7 | 上标 | ^ 和 ^^K |
8 | 下标 | _ 和 ^^A |
9 | 忽略字符 | ^^@ (ASCII 空字符) |
10 | 空格 | ␣ 和 ^^I (ASCII 水平制表符) |
11 | 字母 | A...Z 和 a...z |
12 | 其他字符 | 不在其他类别码中列出的所有内容。最值得注意的是 @。 |
13 | 活动字符 | ~ 和 ^^L (ASCII 换页符) |
14 | 注释字符 | %
|
15 | 无效字符 | ^^? (ASCII 删除) |
活动字符
[edit | edit source]活动字符类似于宏:它们是单个字符,将在任何其他命令之前进行扩展。
\catcode`| = 13
\def|{\TeX}
...
This is a stupid example of |.
|
这是一个关于 TeX 的愚蠢示例。 |
请注意,活动字符需要直接后跟定义,否则编译将失败。
示例
[edit | edit source]- Texinfo
Texinfo 使用类似于 TeX 的语法,但有一个主要区别:所有函数都以 @ 而不是 \
开头。这并非偶然:它实际上使用 TeX 打印文件的 PDF 版本。它基本做的就是输入texinfo.tex它重新定义了控制序列字符。可能的实现
\catcode`\@=0
@def@@{@char64} % To write '@' character.
\catcode`\\=13 @def\{{@tt @char92}}
The @TeX command was previously written '\TeX'. It is now written '@@TeX'.
|
TeX 命令以前写成 '\TeX'。现在写成 '@TeX'。 |
通过这种重新定义,'@' 现在应该引入每个命令,而 '\' 实际上将打印一个反斜杠字符。
- 项目符号
有些人可能发现 LaTeX 列表环境的语法有点繁琐。这里有一个快速定义类似维基的项目符号的方法
\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
[edit | edit source]如果您进行了一些 LaTeX 编程,您一定遇到过这两个命令,\makeatletter
和 \makeatother
。
在 TeX 中,'@' 字符默认属于类别码 11 字母。这意味着您可以将其用于宏名称。LaTeX 利用类别码来指定规则:所有非公共的、内部的宏,其名称中至少包含一个 '@' 字符,这些宏不应由最终用户访问。在文档中,LaTeX 将 '@' 的类别码更改为 12,即 其他。
这就是为什么当您需要访问 LaTeX 内部函数时,必须将所有访问私有函数的命令括在 \makeatletter
和 \makeatother
之间。它们所做的只是更改类别码
\def\makeatletter{\catcode`@ = 11}
\def\makeatother{\catcode`@ = 12}
|
普通 TeX 宏
[edit | edit source]\newcommand
和 \renewcommand
是 LaTeX 特定的控制序列。它们检查没有现有的命令被新定义所覆盖。
在普通 TeX 中,用于宏定义的原语不会对可能的覆盖进行检查。您需要确保没有破坏任何东西。
语法是
\def<macroname>#1<sep1>#2<sep2>{macro content, use of argument #1, blah, #2 ...}
|
您可以在参数之间使用(几乎)任何字符序列。例如,让我们编写一个简单的宏,它将小数点分隔符从点更改为逗号。首先尝试
\def\pointtocomma #1.#2{(#1,#2)}
%%...
\pointtocomma 123.456
|
这将打印 (123,4)56。我们添加了括号只是为了突出显示这里的问题。每个参数是最短的可能的输入序列,与宏定义匹配,包括分隔符。因此 #1
匹配直到第一个点的所有字符,而 #2
仅匹配第一个标记,即 第一个字符,因为它之后没有分隔符。
解决方案:添加第二个分隔符。空格可能看起来很方便
\def\pointtocomma #1.#2 {(#1,#2)}
|
一般来说,每当您希望使用特定分隔符获得多个参数时,都要考虑最后一个分隔符。如果您不想使用分隔符,那么普通 TeX 宏的使用方式与 LaTeX 宏相同(没有默认参数)
\def\mymacro#1#2#3{{\bf #1}#2{\bf #3}}
%% ...
\mymacro{word1}{word2 word3}{!!!}
|
扩展定义
[edit | edit source]TeX 还有另一个定义命令:\edef
,它代表 扩展定义。语法保持不变
\edef<macroname><argumentslist>{<expanded content>}
|
内容在使用 \edef
的地方被扩展(但不会执行,即 打印),而不是在定义的宏被使用的地方。宏扩展并不总是显而易见的...
示例
\def\intro{Example}
\edef\example#1{\intro~---~#1}
\def\intro{Exercise}
\example{This is an example}
|
这里 \intro
的重新定义对 \example
不会有任何影响。
全局定义
[edit | edit source]定义仅限于其范围。但是,有时将宏定义在一个组中,使其在该组之外以及直到文档结束时仍然有效,这可能很方便。这就是我们所说的 全局定义。
{
\def\LocalTeX{Local\TeX}
\global\def\GlobalTeX{Global\TeX}
}
I can still access the \GlobalTeX{} macro here.
|
您也可以将 \global
命令与 \gdef
结合使用。
这两个命令都有快捷方式
\gdef
用于\global\def
\xdef
用于\global\edef
长定义
[edit | edit source]之前的定义命令不允许您在多个段落中使用它们,即 包含 \par
命令(或双行换行符)的文本。
您可以在定义之前加上 \long
命令,以允许使用多段落参数。
示例
\long\def\dummy#1{#1}
\dummy{First paragraph\par Second paragraph}
|
外部定义
[edit | edit source]此前缀宏阻止定义在某些上下文中使用。它有助于合并宏并使其因错误的上下文而更不容易出错。外部宏 旨在在任何上下文之外使用,因此得名。
例如,以下代码将失败
\outer\def\test{a test}
\def\failure{\test}
|
外部宏不允许出现在
- 宏参数
- 跳过的条件
- ...
let 和 futurelet
[edit | edit source]\let<csname><token>
与 \expandafter\def\expandafter<csname>\expandafter{<content>}
相同。它定义了一个新的控制序列名称,该名称等效于指定的 token。该 token 通常是另一个控制序列。
请注意,\let
只会扩展 token 一次,这与 \edef
相反,\edef
将递归扩展,直到不再可能进一步扩展。
示例[1]
Using let:\par
\def\txt{a}
\def\foo{\txt}
\let\bar\foo
\bar % Prints a
\def\txt{b}
\bar % Prints b
Using edef:\par
\def\txt{a}
\def\foo{\txt}
\edef\bar{\foo}
\bar % Prints a
\def\txt{b}
\bar % Prints a
|
\futurelet<csname><token1><token2>...
的工作方式略有不同。首先,token2 被分配给 csname,然后 TeX 处理 <token1><token2>...
序列。因此,\futurelet
允许您在使用标记后立即分配它。
特殊的控制序列名称
[edit | edit source]某些宏的名称可能无法直接写入。对于由宏名称组成的宏名称,情况就是如此。示例
\def\status{full}
\def\varempty{This is empty}
\def\varfull{This is full}
\csname var\status \endcsname
|
最后一行将根据 \status
打印一个句子。
此命令实际上与 \string
相反,\string
会打印一个控制序列名称,而不会扩展它
{\tt \string\TeX}
|
\TeX |
控制扩展
[edit | edit source]\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 类型的內容
- 类别码
- 字符定义
- 字体参数
- 内部参数
- 长度
- 寄存器
- ...
示例
Text dimensions: $ \the\hsize \times \the\vsize $
|
寄存器是一种类型的变量。它们的數量有限,从 0 到 255。共有 6 种不同的类型
类型 | 描述 |
---|---|
盒子 | 一个盒子 |
计数器 | 一个整数 |
尺寸 | 一个长度 |
粘性 (mu 单位) | 一个粘性 (mu 单位) |
粘性 | 一个粘性 |
令牌 | 一个令牌序列 |
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 可以是计数器、尺寸、粘性 (mu 单位) 或粘性类型。它对盒子和令牌没有意义。
基本语法是
\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
|
这是假的 |
你可以使用 \newif
命令创建新的条件语句(作为一种 布尔变量)。通过这些自定义条件语句,你可以以一种优雅的方式控制代码的输出。说明条件语句用法的最佳方法是通过一个例子。
必须生成两个版本的文档。一个版本是针对 A 组的,另一个版本是针对其他所有人的(即不属于 A 组的)。
1. 我们使用 \newif
来定义我们的条件语句(即布尔变量)。
\newif\ifgroupA
|
2. 以下方式为我们的条件语句设置一个值(真或假)
\groupAtrue % or
\groupAfalse
|
也就是说
\<conditionalsname>true
\<conditionalsname>false
|
取决于我们希望在条件语句中设置哪个值。
3. 现在我们可以在之后的任何地方使用我们的条件语句,在 if 控制结构 中。
\ifgroupA
% Here we write the code of the document that is
% intended for the group A
\else
% Here we write the code of the document that is
% intended for the rest of the people
\fi
|
一个完整的例子是
\newif\ifdirector
%I set the conditional to false
\directorfalse
\ifdirector
I write something for the director.
\else
I write something for common people.
\fi
|
我写一些针对普通人的东西。 |
语法是 \ifcase <number><case0>\or<case1>\or...\else<defaultcase>\fi
。如果 number 等于情况编号,它的内容将被打印。注意,它从 0 开始。
\ifcase 2 a\or b\or c\or d\else e\fi
|
c |
\else
用于指定默认情况(当之前的任何情况都没有匹配时)。
基本语法是
\loop <content> \if*<condition><true action>\repeat
|
与往常一样,content 和 true action 是任意的 TeX 内容。\if*
指的是任何 条件语句。注意,没有 false action,你不能在 \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
|
如果在命令之后遇到 plus
或 minus
,\relax
将阻止出现不希望的行为。
\empty
和 \relax
之间的区别在于展开:\empty
在宏展开后会消失。
我们可以使用 \char {charcode}
命令打印所有字符。charcode 实际上是字节值。例如
\char65 = \char `A = \char `\A
|
大多数字符对应于 ASCII 值(例如 A-Za-z),一些字符替换了 ASCII 中的不可打印字符。
你可以定义控制序列以展开为特定字符。语法是 \chardef<control sequence>=<charcode>
。以下序列执行相同的操作。
\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
|
发现 (La)TeX 将所有空白都视为同一类型的间距粘性,这一点令人困惑。Plain TeX 提供了一些命令来保留你写的间距和换行符
\begingroup
\obeylines
\obeyspaces
Relevant text here
\endgroup
|
这意味着你可能需要组合自己的逐字文本环境和你的命令
\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} }{
%% Set the Babel language.
%% Define the command to print the content.
}{
%% Define the command to print nothing.
}
}
|
另一个问题出现了:如何定义一个名称是变量的命令?在大多数编程语言中,这根本不可能。我们可以尝试写的是
\def\#1 #1{#1}
|
它将因两个原因而失败。
- 最后两个“#1”应该指的是新宏的参数,但它们首先扩展到
\localedef
宏的第一个参数,因为它们在该宏的正文中。 \#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}
。它将在{token1}之前扩展{token2}。
最后,如果我们想从命令行设置语言,我们必须能够设置\locale
变量,以便源代码中的变量是默认值,可以被命令行中的变量覆盖。这可以通过\providecommand
来实现
\providecommand\locale{fr}
|
最终代码是
%% Required package.
\usepackage{ifthen}
%% TeX function that generates the language commands.
\def\localedef#1#2{
\ifthenelse{ \equal{\locale}{#1} }{
\selectlanguage{#2}
\expandafter\def\csname#1\endcsname ##1{##1}
}{
\expandafter\def\csname#1\endcsname ##1{}
}
}
%% Selected language. Can be placed anywhere before the language commands.
\providecommand\locale{fr}
%% Language commands.
\localedef{de}{ngerman}
\localedef{en}{english}
\localedef{fr}{frenchb}
%% ...
|
您可以使用以下命令进行编译
latex '\providecommand\locale{en}\input{mydocument.tex}'
- ↑ 来自 tex.stackexchange.com: \let 和 \edef 之间的区别是什么?
- 进一步阅读
- TeXbook,Donald Knuth
- TeX by Topic,Victor Eijkhout
- TeX for the Impatient,Paul W. Abrahams, Karl Berry 和 Kathryn A. Hargreaves
- TeX 命令参考在 wikibooks 中