xparse (\NewDocumentCommand)

当你想创建带有多个可选参数,或带有正式星号变体的命令时,\newcommand 那种只数参数个数的方法就不够用了。现代做法是 \NewDocumentCommand,它用一串字母声明每个参数的 *种类*。它最初来自 xparse 宏包,如今已是 LaTeX 内核的一部分。本页会介绍参数指定的迷你语言,以及用来检查用户实际给了什么参数的判定命令。

现在已是内核的一部分

过去需要 \usepackage{xparse},但 自 2020 年 10 月的 LaTeX 版本起,这套机制已进入 LaTeX 内核\NewDocumentCommand 等命令开箱即用,几乎不再需要载入 xparse。内核中实现此功能的文件称为 ltcmd,而 xparse 宏包本身已被官方描述为“obsolete”。除了少数已弃用的参数类型,新代码可以直接使用这些命令,无需载入任何宏包。

这个现代接口的思想是 把含义(接口)与实现分开。你把用户看到的参数序列(必需、可选、星号)声明为“参数指定(argument specification, arg-spec)”,而命令本体总是以 #1#2 等规范化形式接收参数。\newcommand 的基础在“定义宏”页面已经说明;如果还没读过,先看一遍会更容易看出差异。

定义命令与环境

基本形式是 \NewDocumentCommand{\cmd}{⟨arg-spec⟩}{⟨code⟩}:第一个参数是命令名,第二个是参数指定,第三个是本体。和 \newcommand 一样,它也有针对不同目的的同族命令;区别在于遇到已定义名称时如何处理。

命令遇到已有名称时
\NewDocumentCommand若已定义则报错
\RenewDocumentCommand若尚未定义则报错(重新定义已有命令)
\ProvideDocumentCommand仅在尚未给出时定义
\DeclareDocumentCommand总是覆盖(谨慎使用)

环境也有同样机制,可用 \NewDocumentEnvironment{⟨env⟩}{⟨arg-spec⟩}{⟨start code⟩}{⟨end code⟩} 定义(也有 \Renew…\Provide…\Declare…)。参数在 \begin{⟨env⟩} 之后给出,开始代码和结束代码都能看到;详见“自定义环境”页面。还有一个好处:用这种方式创建的所有命令都会自动 robust(内部使用 ε-TeX 的 \protected 机制),因此在标题或图注中不用担心 \protect

还有一个可展开版本 \NewExpandableDocumentCommand。它可以用于命令会被“展开”的上下文,例如 \edef\write 中;代价是可用的参数类型受限(后述 s/t 这类布尔测试和 v 等不可用)。普通文档中通常使用标准版本就足够。

参数指定语言

参数指定是 一串字符,每个字符表示一个参数的“类型”:必需、可选、星号、由特定字符定界等等。各类型会作为 #1#2 等传到本体中,但关键细节是:未给出的可选参数会得到一个特殊值。主要类型见下表。

类型写法含义传入本体的形式
mm必需的 {} 参数普通的 #1
oo可选的 [...] 参数缺省时为 -NoValue-
OO{default}带默认值的可选参数缺省时为 default
ss可选星号 *\BooleanTrue / \BooleanFalse
tt⟨char⟩可选的指定字符记号\BooleanTrue / \BooleanFalse
rr⟨d1⟩⟨d2⟩d1d2 定界的必需参数缺失时报错后为 -NoValue-
dd⟨d1⟩⟨d2⟩d1d2 定界的可选参数缺省时为 -NoValue-
ee{⟨tokens⟩}装饰项(如 ^_每项缺省时为 -NoValue-
vvverbatim 参数原样字符

m 是最基本的必需参数;它接受花括号组或单个记号,并在传给本体前去掉外层 {}o 是标准可选参数 [...]O{default} 是带默认值的版本。像 OD 这样的 大写类型可以指定默认值,而 od 等小写版本会返回特殊标记 -NoValue-

s 用来检测前导星号 *:有星号时得到 \BooleanTrue,没有时得到 \BooleanFalse。推广到任意单个字符就是 t⟨char⟩;例如 t+ 会把是否有 + 作为布尔值传入。r⟨d1⟩⟨d2⟩ 是由你选择的定界符包住的“必需”定界参数(如 r() 表示圆括号);若缺少开定界符,会报错并插入 -NoValue-d⟨d1⟩⟨d2⟩ 则是可选的定界版本。

稍特殊的是 e{⟨tokens⟩},它收集 ^_ 这样的 装饰项(embellishments)。写 e{^_} 时,它会以任意顺序接收 ^{...}_{...},未给出的项目为 -NoValue-(列出的记号必须彼此不同)。v\verb 一样读取 verbatim 参数。注意,定界类型(rd 等)的定界符不能使用 TeX 分组字符 {};通常选择 []()"" 这样的自然成对字符。

检查参数:判定命令

要处理可选参数或星号,就需要在本体中判断“是否给出”。专用判定命令有两类。首先 \IfNoValueTF{#1}{⟨if no value⟩}{⟨if value⟩} 会检查参数是否为 -NoValue-(即未给出);没有值时执行第一个分支,有值时执行第二个分支。反向的 \IfValueTF 也存在,并且都有只取 T 或只取 F 的版本(\IfNoValueT / \IfNoValueF\IfValueT / \IfValueF)。

对于 st 产生的布尔值,请使用专用的 \IfBooleanTF{#1}{⟨if starred⟩}{⟨if not⟩}#1\BooleanTrue 时执行第一个分支,为 \BooleanFalse 时执行第二个分支(也有 \IfBooleanT / \IfBooleanF)。经验规则是:\IfNoValueTF 系列用于 o/d 等可选参数,\IfBooleanTF 系列用于 s/t 的布尔参数。另外,-NoValue- 被设计成不会与字面文本 -NoValue- 相同,因此务必用 \IfNoValueTF 判断,而不是比较字符串。

示例:星号加带默认值的可选参数

s O{} m 这个参数指定,创建一个同时具有星号变体和带默认值可选参数的命令。这里以标题命令为例:#1 是星号(用 \IfBooleanTF 判断),#2 是可选短标题(默认空),#3 是主标题。

latex
\NewDocumentCommand{\heading}{s O{} m}{%
  \IfBooleanTF{#1}%
    {\textbf{#3}}%                       星つき: 番号なしの見出し / starred: unnumbered
    {\section%
       \IfNoValueTF{#2}%
         {{#3}}%                          短縮見出しなし / no short title
         {[#2]{#3}}%                       短縮見出しあり / with short title
    }%
}

% 使用例 / usage
\heading{Introduction}             % 通常の番号つき節
\heading[Intro]{A Long Introduction} % 目次用に短縮見出しを指定
\heading*{Preface}                 % 星つき: 太字の無番号見出し

一个细节:不要混淆 oO{}。使用 o 时,未给出的参数会变成 -NoValue-,因此可以像上面那样用 \IfNoValueTF 分支。使用 O{} 时则相反,参数 总是 有值(未给出时为空),此时应测试“是否为空”,例如用 \tl_if_blank:nTF(expl3)或 \ifblank(etoolbox),而不是 \IfNoValueTF。上例若把可选参数设为 o(无默认值),\IfNoValueTF 分支才符合意图。

为什么优于 \newcommand

\newcommand 只允许 一个 可选参数,而且必须是第一个;也没有内置的星号变体。传统做法是手写 \@ifstar\@ifnextchar 等低层机制来弥补,但它们需要 \makeatletter,在空白和嵌套方面脆弱,也不易阅读。

  • 多个独立可选参数o o mO{a} O{b} m 等)。
  • 通过 s\IfBooleanTF 得到 真正的星号变体,不需要 \@ifstar 技巧。
  • 声明式默认值:只要写 O{default},未给出时的值就确定了。
  • 一开始就是 robust:标题和图注中不需要 \protect
  • 还能表达 \newcommand 无法表达的输入语法,如定界参数(rd)和装饰项(e)。

总之,简单缩写用 \newcommand 已足够;但 只要参数结构稍微复杂,\NewDocumentCommand 就应成为首选。当你还想把本体写成小程序时,与 expl3(LaTeX3 编程层)配合就是自然的发展方向。类和宏包的编写见“编写类与宏包”页面。