我试图理解输入、扩展和执行处理器如何协同工作。在
\def\foo#1{(#1)\baz}%
\def\baz{baz}%
\foo{bla} Bar
\bye
得到“(bla)baz Bar”,其中 baz 和 Bar 之间有一个空格。我天真地认为这\foo
是扩展的,扩展之后我应该有
\foo{bla} Bar
→(bla)\baz Bar
现在\baz
吞噬了后面的空格。也许,在 的定义中\foo
,\baz
已经被标记化,并且后面的空格不会被吞噬,但这意味着执行处理器已经处理了第一行。或者也许整行已经先被读取,在这种情况下}
和之间的空格字符B
已经转换为空格标记。或者可能是其他我没看到的东西:-)
为什么空间没有被吞噬?
答案1
通常 TeX 会逐行处理输入:
读取整行并进行整行预处理。
- 一步预处理整线是:
构成该行的字符序列的所有字符都是转变从计算机平台的字符表示方案中TeX 引擎的内部字符表示方案。
计算机平台的字符表示方案可能是任何字符编码。对于现代计算机,这通常是 Unicode(并且转换格式通常是 UTF-8)。对于较旧的机器,例如在 MS-DOS 下运行的机器,这可能是某种 8 位编码/字节编码,其中 ASCII(A美国年代标准C颂歌我信息我交换 (nterchange) 是一个子集;例如,在 Win95/Win98/NT 下运行时,这可能是 Windows-1252 或 iso-8859-1/iso-8859-15 或其他。
使用传统的 TeX 引擎TeX-engine 的内部字符表示方案是 ASCII。使用 XeTeX 和 LuaTeX 引擎时,TeX 引擎的内部字符表示方案是 Unicode(ASCII 是其子集)。 - 另一步是整条线的预处理是:
所有空间即,在 TeX 引擎的内部字符表示方案/ASCII/Unicode 中,代码点为数字 32 的所有字符,在该行的右端,被移除。 - 预处理整个生产线的另一步是:
在行的右端附加一个字符TeX 引擎内部字符表示方案中的代码点号等于整数参数的值\endlinechar
。 - 阅读器切换到状态N(新线)。
经过预处理 TeX开始对预处理后的行进行标记。
这意味着 TeX 逐个字符地“查看”预处理后的行,并将字符序列作为一组指令,用于将标记附加到标记流中。字符的类别代码在此发挥作用。
[逐个字符地“查看”预处理后的行,并将标记附加到标记流中,这是“按需”进行的,即,仅当 TeX 需要标记而标记流为空时。例如,当收集宏参数或⟨平衡文本⟩,或者在“查看”是否还有更多工作要做时,因为还没有遇到结束工作的命令——例如 (plain TeX)\bye
或\end
(LaTeX)\stop
或—。 一方面,将另一个值分配给整数参数确实会影响输入行的预处理。因此,分配到不会影响它发生的输入行(但只会影响后续行),因为显然该行在执行分配时已经进行了预处理。 另一方面,更改类别代码可能会影响事物的标记化,而标记化是在预处理后按需进行的。因此,更改类别代码可能会影响在分配更改类别代码之后立即出现的事物(即使在当前行中)的标记化。 更改“结束行字符”的类别代码可能会影响(在当前行的预处理期间已经附加的)当前行的“结束行字符”的标记化方式。\end{document}
\endlinechar
\endlinechar
例如,您可以输入“我不能在课堂上说话!”十次,方法是分配\endlinechar
一个 nice 值并使相应的字符处于活动状态,并定义该活动字符以提供一个水平框,其中包含短语“我不能在课堂上说话!”,然后向 .tex 输入添加十个空行(通过在输入源代码时按回车键十次),在编译期间插入十个结束行字符,因为这十个空行中的每一行都经过预处理 - 请注意,-assignment\endlinechar
不会影响它出现的行(但只会影响后续行),因为该行在执行该 -assignment 时已经经过预处理\endlinechar
。插入的十个结束行字符中的每一个依次被标记为上述活动字符,其中包含短语“我不能在课堂上说话!”:
\begingroup
% Let's make 'A' active:
\catcode`\A=13 %
% Let's have a scratch-counter for counting how many times
% the phrase "I must not talk in class!" is written:
\newcount\scratchcount
% Let's define the active-'A' to do some counting and to
% deliver the line "I must not talk in class!":
\def A{%
% Ensure vertical mode:
\ifvmode\else\par\fi
% Increment the scratch-counter and place the line/
% the horizontal box:
\advance\scratchcount by 1 %
\hbox{\number\scratchcount.\null\ I must not talk in class!}%
}%
% Make the character 'A' the endline-character:
\endlinechar=`\A\relax
% (The \endlinechar-assignment in the line above does not affect
% that line. It does affect subsequent lines only. It does not
% lead to appending the character 'A' to that line as at the time
% of carrying out that assignment in TeX's stomach, that line is
% already pre-processed with the old value of \endlinechar (which
% is 13, denoting the return-character) ).
%
% Now let's have ten empty lines, yielding ten endline-characters
% 'A' whereof each gets tokenized as active-'A' expanding to the
% directives for doing some counting and delivering the line with
% the phrase "I must not talk in class!".
\endgroup%
% The comment-char at the end of the line above must be as the line
% above obviously gets pre-processed _before_ carrying out \endgroup
% and thus it also will have an endlinechar-'A' appended.
% Without the comment-char that 'A' would--as at the time of gathering
% the characters that form the name of the control-word-token '\endgr...'
% the character 'A' is not of category-code 11(letter)--not be taken for
% something that belongs to the name of that "\endgr..."-control-word-token
% and therefore would trigger termination of gathering the name of the
% '\endgr...'-control-word-token and would be put back into the input
% stream.
% After processing/carrying out the control-word-token '\endgroup', 'A'
% is of category-code 11(letter).
% Therefore processing/tokenizing the 'A' that was put back into the
% input-steam would yield an 'A'-character-token of category-code
% 11(letter), at some later stage of processing yielding a glyph 'A'
% within the output-file/within the .dvi- or .pdf-file.
%
% Now let's get the token '\bye' in a funny way:
\endlinechar=`e
\by
]
让我们看看你的代码:
Line 1: \def\foo#1{(#1)\baz}%
Line 2: \def\baz{baz}%
Line 3: \foo{bla} Bar
Line 4: \bye
第 1 行和第 2 行是没有空格的代码行,因此这里没有空格标记。我们在这里不做详细介绍。这些行中的每一行都以百分号字符结尾,而百分号字符的类别代码为 14(注释)。由于整数参数的\endlinechar
值为 13(13 表示 TeX 引擎内部字符表示方案/ASCII/Unicode 中的返回字符),因此在预处理阶段,每个行都会在百分号字符后面附加一个返回字符。但在标记化阶段,类别代码为 14(注释)的字符(当不被视为控制符号标记的名称时)会导致 TeX 停止对当前输入行进行标记,并开始处理下一行输入(如果存在)。因此,一行输入中的百分号字符根本不会导致将标记附加到标记流,而会导致 TeX 默默地“丢弃”它和该行输入的剩余字符。由于附加的返回字符\endlinechar
也属于该行输入的剩余字符,因此它也会被默默地丢弃。
第 3 行经过预处理(由 TeX 的眼睛)如下:
读取该行并将其单个字符转换为 TeX 引擎的内部字符表示方案。
行的右端没有空格。因此,行的右端没有空格需要删除。
由于\endlinechar
(通常)值为 13,而 13 是 ASCII/Unicode/TeX 引擎内部字符表示方案中返回字符的代码点编号,因此(通常)在行末字符后面插入一个返回字符,即r
。通常返回字符的类别代码为 5(行末)。
当 TeX(在它的嘴里)开始对预处理后的行进行标记时,读取装置会切换到状态 N(新行)。
(当阅读器处于状态N(新行)时,则
- 空格字符根本不会产生附加标记到标记流中,而只是被丢弃,并且
- 类别代码为 5(行尾)的字符会将控制字标记附加
\par
到标记流,并且还会导致 TeX 停止对当前行的剩余字符进行标记/还会导致 TeX 删除当前行的剩余字符并开始处理下一行输入(如果存在)。
)
因此,TeX 的嘴巴会逐渐地,即,每当需要标记时,就会对预处理的行/预处理的输入字符序列进行标记(现在转换为 TeX 引擎的内部字符表示方案)
\foo{bla}⟨space-character⟩Bar⟨return-character⟩
如下:
控制字标记
\foo
。(将控制字标记附加到标记流后,阅读器切换到状态 S(跳过空白)。)就像
\foo
处理参数的宏一样,需要通过标记更多输入来获取参数:{
类别代码为 1(开始组)的显式字符标记(开始花括号)。(在附加非类别代码 10(空格)的显式字符标记之后,或者在附加不同于控制空格(\
)的控制符号标记之后,阅读装置切换到状态 M(行中)。)- 类别代码为 11(字母)的显式字符标记
b
。(在附加非类别代码 10(空格)的显式字符标记之后,或者在附加不同于控制空格(\
)的控制符号标记之后,阅读器切换到状态 M(行中)。) - 类别代码为 11(字母)的显式字符标记
l
。(在附加非类别代码 10(空格)的显式字符标记之后,或者在附加不同于控制空格(\
)的控制符号标记之后,阅读器切换到状态 M(行中)。) - 类别代码为 11(字母)的显式字符标记
a
。(在附加非类别代码 10(空格)的显式字符标记之后,或者在附加不同于控制空格(\
)的控制符号标记之后,阅读器切换到状态 M(行中)。) }
分类代码为 2(结束组)的显式字符标记(结束花括号)。(在附加一个不属于分类代码 10(空格)的显式字符标记之后,或者在附加一个不同于控制空格(\
)的控制符号标记之后,阅读器将切换到状态 M(行中)。)因此,以下标记现在从 TeX 的嘴巴发送到 TeX 的胃中 - 在到达胃的途中,这些标记会经过 TeX 的食道,在那里进行扩展: 而 TeX 的嘴巴仍然保留着剩余的预处理过的输入字符序列。
\foo(control-word-token){1(begin-group)b11(letter)l11(letter)a11(letter)}2(end group)
⟨space-character⟩Bar⟨return-character⟩
在通过 TeX 的喉咙时,这些标记的扩展会产生:
\foo
需要非分隔参数。显式空格标记前A非定界在收集构成参数的标记时,宏参数会被丢弃。(非分隔参数要么是单个标记(既不是显式空格标记,也不是类别代码 1(开始组)的显式字符标记,也不是类别代码 2(结束组)的显式字符标记,也不是 - 标记\outer
)或由一对匹配的花括号(左括号和右括号)组成,其中嵌套了一组括号平衡的非 -\outer
标记。该括号平衡的标记集可以是“空的”。)如果存在,则在传递宏的替换文本时,将丢弃一对围绕整个宏参数(无论是分隔的还是非分隔的宏参数)的匹配花括号。
扩展\foo
产生以下替换:(12(other)b11(letter)l11(letter)a11(letter))12(other)\baz(control-word-token)
嘴里仍然保存着剩余的预处理过的输入字符序列。
⟨space-character⟩Bar⟨return-character⟩
当这些标记滑入食道时,可扩展的控制字标记
\baz
也会随之扩展 - 以下标记到达了 TeX 的胃部:(12(other)b11(letter)l11(letter)a11(letter))12(other)b11(letter)a11(letter)z11(letter)
在胃中处理这些标记(在其中进行分配、构建框、将段落分成多行、将行放置在页面上等)会导致切换到水平模式并将字形序列添加
(bla)baz
到水平列表中,从中将构建输出文件/.pdf 文件的下一行文本。TeX 的嘴里仍然保存着剩余的预处理过的输入字符序列。
⟨space-character⟩Bar⟨return-character⟩
没有迹象表明这项工作即将完成,因此 TeX 继续进行其消化过程:
读取装置既不处于状态 N(换行符),也不处于状态 S(跳过空格),而是处于状态 M(行中间),并且 TeX 未收集控制符号标记的名称。因此,从其口中剩余的预处理输入字符序列中, 它将标记 为显式空格标记(字符代码 32,类别代码 10(空格)),并将其附加到标记流/将其沿其食道发送到胃中。 (在附加类别代码 10(空格)的显式字符标记或附加控制空格()后,读取装置切换到状态 S(跳过空格)。) 由于 TeX 处于水平模式,胃中的空格标记导致 TeX 将水平粘连添加到水平列表,这反过来(如果没有因某种原因被丢弃)会在 .pdf 输出文件中产生可见的水平空白空间。
⟨space-character⟩Bar⟨return-character⟩
⟨space-character⟩
\
TeX 的嘴保存着剩余的预处理过的输入字符序列。
Bar⟨return-character⟩
没有迹象表明这项工作即将完成,因此 TeX 继续进行其消化过程:
它从嘴里剩余的预处理输入字符序列中标记出
B
类别代码 11(字母)的明确字符标记,并将其顺着食道送入胃中。(在附加一个不是类别代码 10(空格)的明确字符标记或附加一个不同于控制空格()的控制符号标记后\
,阅读器切换到状态 M(行中)。)TeX 的嘴保存着剩余的预处理过的输入字符序列。
ar⟨return-character⟩
没有迹象表明这项工作即将完成,因此 TeX 继续进行其消化过程:
它从嘴里剩余的预处理输入字符序列中标记出
a
类别代码 11(字母)的明确字符标记,并将其顺着食道送入胃中。(在附加一个不是类别代码 10(空格)的明确字符标记或附加一个不同于控制空格()的控制符号标记后\
,阅读器切换到状态 M(行中)。)TeX 的嘴保存着剩余的预处理过的输入字符序列。
r⟨return-character⟩
没有迹象表明这项工作即将完成,因此 TeX 继续进行其消化过程:
它从嘴里剩余的预处理输入字符序列中标记出
r
类别代码 11(字母)的明确字符标记,并将其顺着食道送入胃中。(在附加一个不是类别代码 10(空格)的明确字符标记或附加一个不同于控制空格()的控制符号标记后\
,阅读器切换到状态 M(行中)。)TeX 的嘴保存着剩余的预处理过的输入字符序列
⟨return-character⟩
。没有迹象表明这项工作即将完成,因此 TeX 继续进行其消化过程:
由于 TeX 没有收集控制符号标记的名称,并且读取设备处于状态 M(行中),而返回字符具有类别代码 5(行尾),因此 TeX 将附加到标记流并向其喉咙发送一个明确的空格标记(字符代码 32,类别代码 10(空格))。
(如果 TeX 遇到了一个类别代码为 5(行尾)的字符,而读取设备处于状态 N(新行),并且 TeX 没有收集控制符号标记的名称,那么 TeX 会将控制字标记附加到
\par
标记流中。
这就是为什么在正常情况下- 源代码中的空行和
- 源代码中的行只包含空格字符和
- 源代码中的行仅包含类别代码 9(忽略)和 10(空格)的字符混合,该混合字符后面可能还跟着一些空格字符
产生控制字标记\par
。(在每种情况下,该行中的任何字符(如果存在)都不会导致将标记插入到标记流中,因此,当读取装置遇到由于在预处理该\endlinechar
行的阶段行右端的值而插入的类别代码为 5 的返回字符(行尾)时,读取装置仍处于状态 N。)如果 TeX 遇到类别代码 5(行尾)的字符,而读取设备处于状态 S(跳过空格),并且 TeX 没有收集控制符号标记的名称,则 TeX 根本不会将标记附加到标记流中。)
当遇到类别代码 5(行尾)的字符而未收集控制符号标记的名称时,TeX 会在任何情况下停止对当前行进行标记,即删除当前行上的任何剩余字符,并开始处理下一行(如果存在)。
没有迹象表明这项工作即将完成,因此 TeX 继续进行消化过程:
嘴里没有剩余的字符,因此 TeX 的眼睛开始预处理下一行输入。阅读器切换到状态 N(新行)。预处理行的单个字符根据需要进入 TeX 的嘴里,在那里根据需要形成标记。标记根据需要从 TeX 的嘴里发送到 TeX 的胃里。这样它们就通过了 TeX 的食道,可扩展标记在那里被扩展/被替换文本替换。在胃里进行分配,构建框,段落被分成多行,行被放置在页面上等等……
答案2
通常使用当前 catcode 设置将字符标记为字符标记,但在看到 catcode 0 的字符后,它将不会被标记,并且以下字符将用于制作 csname 标记。
在这种情况下,以下字符是b
catcode 11,因此 tex 将读取以下所有 catcode 11 个字符,包括第一个非 catcode 11 字符或行尾。
因此,这里的 catcode 11 个字符序列baz
将构成一个带有名称的 csname 标记,baz
用于终止 csname 扫描的非 catcode11 字符将返回到输入流(作为字符,仍未被标记)除非它是 catcode 10 个空格字符在这种情况下,它将被丢弃,tex 将进入其跳过空白状态,因此任何后续空格也将被丢弃。如果扫描因行尾而终止,则 tex 将直接进入其行首状态,而不添加通常在行尾产生空格的标记,并且下一行开头的所有空格将照常被丢弃。
因此,在您的情况下,后面的字符在第一个定义中,\baz
在第二个定义中,因此不涉及特殊的空格处理,只是在您以后的建议使用中明确将 非 catcode 11 个字符视为空格并丢弃。}
{
(bla)\baz Bar
当宏被扩展时,替换文本是标记列表,因此根本不涉及任何字符到标记或 catcode 查找。
答案3
让我修改你的代码
\def\foo#1{(#1)\baz}
\def\baz{baz}
\foo{bla} Bar\baz Gnu
\bye
这些定义其实并不重要。当 TeX 读取输入时,它会对其进行标记;因此让我们计算相关行中的标记:
\foo
•{
1 •b
11 •l
11 •a
11 •}
2 •⍽
10 •B
11 • 11 • 11 • • 11 •a
11 • 11 • 10r
\baz
G
n
u
⍽
我还添加了类别代码(如果可能);控制序列标记没有类别代码。最后一个空格标记由结束行生成。
后面没有空格标记\baz
,因为在标记化过程中,控制字后的空格会被忽略。
现在 TeX 开始从左侧开始扩展宏。由于\foo
是单参数宏,后面跟着{
1,因此参数是直到匹配}
2的所有内容。因此 TeX 删除所有这些标记,并用定义时保存的替换文本替换它们:
(
12 •b
11 •l
11 •a
11 •)
12 •\baz
•⍽
10 •B
11 •a
11 •r
11 •\baz
•G
11 •n
11 •u
11 •⍽
10
到 的 token\baz
会被传递到下一阶段,剩下
\baz
•⍽
10 •B
11 •a
11 •r
11 •\baz
•G
11 •n
11 •u
11 •⍽
10
现在\baz
是一个无参数宏,因此不会查找未分隔的参数,这将忽略空格;替换会留下
b
11 •a
11 •z
11 •⍽
10 • 11 • 11 • 11 • •B
11 • 11 • 11 • 10a
r
\baz
G
n
u
⍽
请注意,TeX 在此阶段不会进行标记化,因此控制序列后的空格不是忽略。
执行宏替换时,TeX 使用已经形成的标记;因此,\baz
第三个显示的标记列表的开头实际上是标记的“内部”表示。后面的空格是不是忽略。
这是必要的。假设你有
\def\foo#1{#1 is good}
\def\egreg{EG}
然后你想要那个\foo{EG}
或\foo\egreg
打印相同的,而不管传递给的参数是什么\foo
。定义中的参数后面有一个空格,因此在宏替换之后也是如此将要是一个空间。
笔记,上面的描述是对实际发生情况的简化。行不会立即被标记:只扫描行中需要的部分。因此 TeX 实际上开始标记\foo
,在找到一个单参数宏后,它会查找随之而来的内容,即开括号,因此 TeX 会进行标记,直到找到匹配的闭括号。依此类推。但是,由于不涉及类别代码更改,因此假装 TeX 会立即标记整行并不是事实,但对于手头的任务来说,这是一个很好的近似值。
立即标记一行会有什么问题?考虑一下
\catcode`?=\active ?
如果立即对行进行标记,则将?
分配类别代码 12 而不是 13。相反,在需要时进行标记可以解决问题。第二个?
被标记后类别代码分配已完成。
答案4
在您的源文档中,您写道:
\foo{bla} Bar
TeX 的眼睛会将其转换为 token。通常,您会得到 10 个 token。它们是名称为 的控制序列foo
和 9 个字符 token。每个字符 token 都有一个类别。您会得到 6 个字母、一个 begin-group、一个 end-group 和一个空格。
如果你写
\foo {bla}Bar
你只会得到 9 个标记。你不会得到空格。这是因为 TeX 的眼睛会忽略控制序列(例如)后的所有空格\foo
。事实证明这很有用。
我已经解释了 TeX 的工作原理,但没有解释为什么你会感到困惑。TeX 宏不会将文本扩展为文本。它们将标记扩展为标记。这并不是说 TeX 宏会创建第二个源文档,然后再重新读取。希望这对你有所帮助。
最后,行尾有一个行尾字符。那是另外一回事了。(一行结尾是一个空格,连续两个行尾将转换为\par
.)
本答案基于第 7 章,TeX 如何读取你输入的内容在 Don Knuth 的TeXbook。这也是@egreg 的答案,简化了以回答您的问题。
重读这个答案,我意识到这句话控制序列具有两种含义。这可能让你感到困惑。让我来澄清一下。呼叫\foo
,后跟非字母,控制序列文字。当 TeX 的眼睛读取时,它会产生一个控制序列标记,其名字是foo
。
例如,在 Python 中,“Hello world” 是一个字符串文字,它在编译时会生成一个字符串(其值为“Hello world”)。当然,在 Python 中还有其他方法可以获取字符串。例如“Hello”+“world”。同样,TeX 也有命令\csname ... \endcsname
。