LaTeX 团队着手编写通用参数解析器 xparse 是在 1990 年代末,而它的核心 \NewDocumentCommand 直到 2020 年 10 月 1 日的发行版才从实验性宏包毕业、正式进入 LaTeX 内核。二十多年的学徒期是有缘由的。\newcommand 只能数出命令有几个参数,而 \NewDocumentCommand 用一串字母——称为 参数规格(argument specification, arg-spec)——描述每个参数是什么类型。从「数个数」到「作描述」的这一步,换来的是带星号的变体、多个彼此独立的可选参数,以及由自选定界符围起来的参数,这些语法 \newcommand 一个都写不出来。本页逐个讲清每个指定符承诺了什么,什么时候该用 \IfNoValueTF 而不是 \IfBooleanTF,以及什么时候压根不该动用这套机制。
\newcommand 写不出的形状——可选参数只能有一个,且必须在最前
\newcommand 只能造出一种形状:最多一个方括号可选参数,而且只能放在最前面,其后跟零个或多个必需参数。想要更丰富的语法,就只能落到 TeX 的 \def 原语和底层宏编程上——LaTeX News 32 正是这样记录当年处境的。所以老宏包的源码里满是手写的「窥探下一个记号」的装置:用 \@ifstar 看有没有星号,用 \@ifnextchar 看有没有某个字符。这些名字里带 @,必须用 \makeatletter 包起来;它们在空格和嵌套面前很容易失效,而且读起来毫无乐趣。
\NewDocumentCommand 把这些窥探工作换成了一套 声明式语法。你交给它的不是数字而是一串字母,解析器负责读取输入,并始终以规范化的 #1、#2 等形式送进命令本体。这样一来,用户看到的接口就与实现代码分离了。在内核里,这套机制以 ltcmd 这个模块名实现;自 2020 年 10 月 1 日的发行版起,\usepackage{xparse} 已无必要。xparse 宏包如今仍在 CTAN 上,但收录它的 l3packages 套件的 README 已把自己标题为「Deprecated」,并说明这些材料是为了让旧文件继续能编译而保留的。例外是 g/G、l、u 这几个已弃用的参数类型:一旦用上就会得到 Invalid argument type "g" in command "\zzz" (requires xparse). 新写的代码基本没有理由去碰它们。
\NewDocumentCommand 的写法,以及 New / Renew / Provide / Declare 的区别
基本形式是三个参数:\NewDocumentCommand{\cmd}{⟨arg-spec⟩}{⟨本体⟩}——命令名、参数规格、命令本体,本体中用 #1、#2 等接收参数。换掉开头的动词,就改变了遇到已有名称时的态度。用 \NewDocumentCommand 去定义一个已被占用的名字,会停在 LaTeX cmd Error: Command "\section" already defined. 这是一道安全网而不是刁难:想覆盖已有定义就用 \RenewDocumentCommand,只在尚未定义时才定义就用 \ProvideDocumentCommand。
| 定义命令 | 遇到已定义名称时的行为 |
|---|---|
\NewDocumentCommand | 已定义则报错停止;这是默认选择 |
\RenewDocumentCommand | 尚未定义则报错;用于改写已有命令 |
\ProvideDocumentCommand | 只在尚未定义时才定义;宏包用它填补兼容性空缺 |
\DeclareDocumentCommand | 无条件覆盖;官方文档特意叮嘱「谨慎使用」 |
用这四个命令造出来的命令,还附带一个你没要求过的性质:从一开始就是 robust 的。对定义好的命令使用 \meaning,终端会显示 \protected macro:->…,可见底层用的是 ε-TeX 的 \protected 机制。把这样的命令放进标题、图表题注这类「可动参数」里,也不必前置 \protect。至于 \newcommand 定义的命令为什么恰恰在那些位置会坏掉、\DeclareRobustCommand 又在做什么,请见「定义宏」一页。
参数指定符一览——m o O{} s t r d e v b 各表示什么
参数规格是一串 一个字母对应一个参数 的字符,指定符分成两族。必需族是 m、r、R、v、b,可选族是 o、O、d、D、s、t、e、E。贯穿其中的规则只有一条:大写类型允许你指定默认值,小写类型则改为返回特殊标记 -NoValue-。把 o 与 O{...}、d 与 D、e 与 E、r 与 R 成对来看,这条规则每次都成立。官方文档还透露,o、d、O 在内部不过是构造得当的 D 型参数的捷径。
| 指定符 | 含义 | 在本体中如何到达 |
|---|---|---|
m | 必需参数:花括号组或单个记号皆可 | 普通的 #1,外层花括号已去掉 |
r | r⟨d1⟩⟨d2⟩——由自选定界符围起的必需参数 | 缺开定界符时报错后给出 -NoValue- |
R | R⟨d1⟩⟨d2⟩{默认值}——与 r 相同,但恢复值由你决定 | 缺失时给出你写的默认值 |
v | 像 \verb 那样读取的 verbatim 参数;定界字符不能是 % \ # { } 或空格 | 原样字符;不能出现在别的命令的参数里 |
b | 环境的本体;只能用于 \NewDocumentEnvironment,且必须放在最后 | \begin 与 \end 之间的全部内容 |
o | 标准的 [...] 可选参数 | 未给出时为 -NoValue- |
O | O{默认值}——带默认值的 o | 缺省时为默认值;因此永远有值 |
d | d⟨d1⟩⟨d2⟩——由自选定界符围起的可选参数 | 未给出时为 -NoValue- |
D | D⟨d1⟩⟨d2⟩{默认值}——带默认值的 d | 缺省时为默认值 |
s | 检测开头是否有星号 * | \BooleanTrue 或 \BooleanFalse |
t | t⟨char⟩——检测指定的某个字符是否存在,是 s 的推广 | \BooleanTrue 或 \BooleanFalse |
e | e{⟨tokens⟩}——^、_ 之类的装饰记号集合,所列记号必须互不相同 | 每个记号对应一个参数,缺席者为 -NoValue- |
E | E{⟨tokens⟩}{⟨默认值列表⟩}——带默认值的 e | 默认值列表较短时,其余退回 -NoValue- |
定界参数(r、R、d、D)有几条必须记住的限制。首先,TeX 的分组字符 { 和 } 不能充当定界符:写 r{} 会被 LaTeX cmd Error: Argument delimiter "" invalid in command "\zzz". 挡回来。惯例是选 []、()、<>、"" 这类天然成对的字符。其次,当定界符是 字符记号 时,解析器会记住定义那一刻的类别码。若日后把 < 改成字母类,同一个 < 就不再被认作定界符了。反过来,用控制序列(例如 \x)作定界符则不受影响,因为它按名字识别,与当前含义无关。
% t<char> tests for one character; r()...() is a required delimited argument
\NewDocumentCommand{\pt}{t+ r()}{%
\IfBooleanTF{#1}{\mathbf{(#2)}}{(#2)}%
}
$\pt(1,2)$ % -> (1,2)
$\pt+(3,4)$ % -> (3,4) in bold
% e{^} picks up an optional ^ embellishment wherever it appears
\NewDocumentCommand{\deriv}{e{^} m m}{%
\frac{\mathrm{d}\IfNoValueF{#1}{^{#1}}#3}{\mathrm{d}#2\IfNoValueF{#1}{^{#1}}}%
}
$\deriv{x}{f}$ % -> df/dx
$\deriv^{2}{x}{f}$ % -> d^2 f / dx^2+ ! > =——加在指定符前面的修饰字符
+ 让某个参数变成 长(long)参数,从而能吞下空行、也就是分段。从 \newcommand 转过来的人在这里会踩到第一颗地雷:默认值是反过来的。 \newcommand 让所有参数都是长的,想要短参数才写带星号的 \newcommand*;\NewDocumentCommand 正相反,参数默认是短的,只在需要长参数的那个前面写 +。所以刚移植过来的命令一旦接到含空行的正文,就会迎来 ! Paragraph ended before \remark was complete. 不过逐个参数指定正是它的价值所在:一条声明就能表达「短标题只许一段,正文可以多段」。
余下三个可以简短交代。! 表示「不允许可选参数前有空格」,而且只能加在 位于末尾 的可选参数上——放在最前面会得到 Invalid argument prefix "!" in command "\remark". 当你希望 \foo{x} [x] 里的方括号被当作正文时,它正好派上用场。> 用来插入 参数处理器:写 >{\SplitArgument{2}{;}} m,a;b;c 就会先被拆成三个参数再交给本体。内核自带 \SplitArgument、\SplitList、\TrimSpaces、\ProcessList 和 \ReverseBoolean。= 是较新的修饰符,强制把可选参数解释为 键值对;它的出现是为了让 \caption 和各级标题命令这些长期接受自由文本的命令,能在不破坏旧语法的前提下长出键值接口。
% + makes ONE argument long; ! on a trailing optional argument forbids a space
\NewDocumentCommand{\remark}{+m !o}{\par\textbf{Note.} #1 (#2)\par}
\remark{first paragraph
second paragraph}[tag]
\remark{x} [these brackets stay ordinary text]
% > runs a processor before the body sees the argument
\NewDocumentCommand{\triple}{>{\SplitArgument{2}{;}} m}{\tripleaux#1}
\NewDocumentCommand{\tripleaux}{m m m}{(#1/#2/#3)}
\triple{a;b;c} % -> (a/b/c)\IfNoValueTF 与 \IfBooleanTF 怎么分工,以及 o 和 O{} 的区别
判定命令分两族,由指定符决定用哪一族。o、d、e 这类返回 -NoValue- 的类型,用 \IfNoValueTF{#1}{⟨未给出时⟩}{⟨已给出时⟩};s、t 这类返回布尔值的类型,用 \IfBooleanTF{#1}{⟨真⟩}{⟨假⟩}。逻辑相反的 \IfValueTF 也在,两族都备有只取单分支的 \IfNoValueT、\IfNoValueF、\IfValueT、\IfValueF、\IfBooleanT、\IfBooleanF。\IfNoValueTF 之所以必须存在,这件事本身就很有意思:「省略了的可选参数」与「给成空值的可选参数」是两回事。 \newcommand 的默认值机制根本表达不出这个区别——未给出时默认值就直接顶上,而「用户压根没写」这个事实永远传不到命令本体。
-NoValue- 是一道做得很讲究的防伪:它被特意构造成与字面文本 -NoValue- 不相等,因此 \IfNoValueTF{-NoValue-} 在逻辑上为假。所以拿字符串比较来顶替是行不通的——一律用 \IfNoValueTF 判定。最经典的陷阱是把 o 和 O{} 搞混。用 o 时未给出的参数确实是 -NoValue-,\IfNoValueTF 会正确分支;用 O{} 时则 永远有值(未给出时是空串),于是 \IfNoValueTF 总是走假分支。而如果干脆忘了判定,往往要等到排版出的 PDF 里赫然印着 -NoValue- 才发现。
那么想知道 O{} 的内容是不是空的,该用什么?这正是官方建议在 2022 年 6 月改动的地方。内核提供了 \IfBlankTF(还有 \IfBlankT 和 \IfBlankF),它在参数真正为空或只含空白时返回真。对于并排出现两个可选参数的设计,官方文档如今建议用 O{} 搭配 \IfBlankTF,而不是分别去检查「空」和 -NoValue-。不必再特意搬出 expl3 的 \tl_if_blank:nTF 或 etoolbox 的 \ifblank。有个细节:\IfBlankTF 会把 \space 这类命令算作「有内容」——它输出的是空白,但作为记号是实实在在的。
% s = optional star, o = optional [..], m = mandatory
\NewDocumentCommand{\heading}{s o m}{%
\IfBooleanTF{#1}
{\section*{#3}}% starred: unnumbered
{\IfNoValueTF{#2}
{\section{#3}}% no short title given
{\section[#2]{#3}}}% short title for the ToC
}
\heading{A Long Introduction} % numbered section
\heading[Intro]{A Long Introduction} % short title in the table of contents
\heading*{Preface} % unnumbered
% with O{} the value is always there, so test for blankness instead
\NewDocumentCommand{\tagged}{O{} m}{\IfBlankTF{#1}{#2}{[#1] #2}}这个标题例子里还藏着一个 \newcommand 学不来的性质:用 \NewDocumentCommand 造出的可选参数可以安全嵌套。 借用官方文档的例子,\foo[\baz[stuff]]{more stuff} 能被正确解析,尽管可选参数里又装着一个自带可选参数的命令。\newcommand 的方括号只会朴素地取到「下一个 ]」为止,同样的输入就会在内层的 ] 处被截断。一旦你想把带可选参数的命令放进另一个可选参数里,这就足以构成改用 \NewDocumentCommand 的理由。
\NewDocumentEnvironment 与 b 型——把环境的内容当作参数接收
环境有一套一模一样的机制,用 \NewDocumentEnvironment{⟨env⟩}{⟨arg-spec⟩}{⟨开始代码⟩}{⟨结束代码⟩} 定义(同样有 \Renew…、\Provide…、\Declare…)。参数紧跟在 \begin{⟨env⟩} 之后给出,开始代码和结束代码都能取用。这里还多出一个命令一侧没有的指定符——b,也就是环境本体本身。把 b 放在参数规格的末尾,\begin 与 \end 之间的全部内容便会作为单个参数送达,可以加工、可以排两遍,也可以按条件丢弃。
用 b 有三条规矩。第一,本体默认会 去掉首尾空白,因此不必操心行末的空格;若想保留,写成 !b。第二,本体可能跨多个段落时,写成 +b。第三——这条最容易忘——一旦用了 b,结束代码实际上就多余了,但 那个空的第四个参数仍然必须写出来;漏掉它,\NewDocumentEnvironment 就会数错参数。另外,使用 b 的环境彼此之间可以嵌套。\newenvironment 的基础,以及不需要 b 的普通环境写法,见「自定义环境」一页。
% b grabs the whole body; + allows several paragraphs; the empty 4th
% argument is still required even though there is no end code left to run
\NewDocumentEnvironment{shout}{O{\bfseries} +b}{#1#2}{}
\begin{shout}[\itshape]
Loud and clear.
\end{shout}什么时候需要 \NewExpandableDocumentCommand——表格单元格的开头与 \edef 内部
标准版造出的是 robust 命令——不会轻易被展开——这在绝大多数场合是优点,偶尔却成了阻碍。最实际的例子是 表格单元格的开头:标准 tabular 机制要求包裹 \multicolumn 的命令必须可展开,而 \NewDocumentCommand 造的命令借助引擎特性刻意阻止展开,因此在这里用不了。想在 \edef 或 \write 中把内容确定下来时也是同样。这正是 \NewExpandableDocumentCommand(以及 \Renew…、\Provide…、\Declare…)存在的理由。不过官方文档说得很直白:只在确有必要时 才用它,因为它带着一串限制。
- 只要带有参数,最后一个参数必须是
m、r或R之一,也就是必需类型。 - verbatim 类型
v不可用,>的参数处理器和=的键值修饰符同样不可用。 - 它分不清
\foo[与\foo{[}:两处的[都会被当作可选参数的开始,因此可选参数的识别不如标准版稳健。 - 另一方面,
s和t这类布尔参数是可以用的。\IfBooleanTF本身可展开,所以即使在\edef里分支也如预期般解析。
% a command wrapping \multicolumn must be expandable to work in a cell
\NewExpandableDocumentCommand{\wide}{m}{\multicolumn{3}{c}{#1}}
\begin{tabular}{lcr}
a & b & c \\
\wide{spans three columns} \\
\end{tabular}\newcommand 和 \NewDocumentCommand 该用哪一个
如果只是没有参数、或者带一两个必需参数的缩写宏,\newcommand 完全够用。 把 \newcommand{\R}{\mathbb{R}} 改写成 \NewDocumentCommand,除了变长以外一无所得。\newcommand 既没过时也未被弃用,在新接口出现之后它依然是 LaTeX 的正规工具。而该换用的信号相当清晰:想要带星号的变体时、需要第二个可选参数时、输入语法不该是 [...] 时,以及必须把带可选参数的命令放进另一个可选参数里时。 只要沾上其中一条,写一行参数规格就比手写 \@ifstar 那套管道更短、也更易读。
最后再给一条方向相反的准则。\NewDocumentCommand 是 设计输入语法的工具,而不是编写命令本体逻辑的语言。当你在参数到手之后开始拆分字符串、层层嵌套条件或做循环时,就已经踏入 expl3(LaTeX3 编程层)的地界了——毕竟 ltcmd 本身就是用 expl3 写的。反过来说,在宏包或文档类里设计面向用户的命令时,\NewDocumentCommand 是首选,因为那串参数规格本身就可以当作接口说明书来读。