在 LaTeX 里自定义命令(宏)最重要的理由,并不是少敲几个键。设想一篇学位论文里所有向量都写成 \mathbf{v},一共四百处,而导师此刻改口说想用箭头。若当初写的是 \vect{v},背后只有一条 \newcommand,那么整个改动就是导言区的一行;否则就是四百次小心翼翼的替换。宏是留给自己日后改主意的地方。本页从 \newcommand 和它的参数讲起,理清 \renewcommand 与 \providecommand 的分工,解释宏名后面的空格为什么会消失,最后谈 fragile 命令、\protect 与现代的 \NewDocumentCommand。
用 \newcommand 定义自己的命令
写法只有一行:\newcommand{\name}{definition}。第一个参数是想要的命令名,第二个参数是它代表的内容;此后每次输入 \name,都会被替换成 definition。放置的位置一般是导言区,也就是 \begin{document} 之前。开头那个向量的例子价值就在这里:\vect 这个名字指的是 含义,而不是外观。写下「这是一个向量」而不是「这里要加粗」,就把「究竟用粗体还是加箭头」这个判断留在了唯一的一处。这与 LaTeX 自身的设计如出一辙——正文里写 \section 而不是手动指定 14pt 粗体,做的正是同一笔交易。
% preamble: one line decides how every vector in the document looks
\usepackage{amsmath,amssymb}
\newcommand{\vect}[1]{\mathbf{#1}}
% \newcommand{\vect}[1]{\vec{#1}} % swap this line, the whole thesis follows
% semantic names for things you refer to constantly
\newcommand{\R}{\mathbb{R}}
\newcommand{\dd}{\mathrm{d}}
% body
\[ \vect{v} \cdot \vect{w} = \lvert \vect{v} \rvert \, \lvert \vect{w} \rvert \cos\theta \]
\[ \int_{\R} f(x) \, \dd x \]带参数的宏,以及可选参数
若要让内容随每次调用而变化,就在命令名后的方括号里写上 参数个数,并在定义中用 #1、#2 等接收,也就是 \newcommand{\name}[⟨nargs⟩]{... #1 #2 ...}。这里有且只有一个硬上限:参数只能从 #1 到 #9,最多九个。有意思的是,要第十个参数时出现的 ! You already have nine parameters. 这条错误,来自 TeX 引擎本身而不是 LaTeX。这个限制属于底层的 \def 原语,\newcommand 无从放宽。真需要超过九个时,通常意味着该把设计改成键值选项,而不是位置参数。
% two mandatory arguments: a number and a unit
\newcommand{\unit}[2]{#1\,\mathrm{#2}}
$a = \unit{9.8}{m/s^2}$再进一步,还可以 把第一个参数变成可选并给出默认值。写法是两层方括号的 \newcommand{\name}[⟨nargs⟩][⟨default⟩]{...}:#1 成为可选参数,不写时代入 ⟨default⟩。调用时写 \name{...}(用默认值)或 \name[x]{...}(把 #1 设为 x),其余必需参数从 #2 数起。陷阱正在计数方式上:[⟨nargs⟩] 是 包含可选参数在内的参数总数。下例的 [2][2] 意思是「两个参数,其中第一个可选、默认值为 2」。
% two arguments in total; the first is optional and defaults to 2
\newcommand{\pow}[2][2]{(x + y)^{#1}_{#2}}
$\pow{n}$ % -> (x + y)^2_n
$\pow[3]{n}$ % -> (x + y)^3_n
% starred form: the argument may not contain a blank line
\newcommand*{\keyword}[1]{\textsf{#1}}再补两个细节。其一,不写 [⟨default⟩] 与写一对空括号 [] 并不相同:后者给出的是「默认值为空字符串的可选参数」。其二,带星号的 \newcommand* 创建的是「short」宏,其参数中不允许出现空行(即 \par)。这看似限制,实为诊断:忘了右花括号时,! Paragraph ended before \keyword was complete. 会在 出错位置附近 报出来。不加星号的话,TeX 会心安理得地把下一段、再下一段都当成参数继续读,错误要到好几页之后才浮现。凡是本就不该跨段落的宏,都加上 *。
\newcommand、\renewcommand 与 \providecommand 的区别
三者接收参数的写法完全一样,区别只在于 遇到已被占用的名字时的态度。\newcommand 拒绝并停止,\renewcommand 直接覆盖,\providecommand 则安静让位、保留已有定义。因此把 \newcommand 用在已存在的名字上会以 ! LaTeX Error: Command \emph already defined. 中断;反过来把 \renewcommand 用在未定义的名字上会以 ! LaTeX Error: Command \foo undefined. 中断。这两条错误正好成对,从两侧分别防住 误覆盖了别人的命令 和 以为覆盖了其实什么也没改 这两种事故。
这里有个怪事:写 \newcommand{\endnotes}{...} 会以 Command \endnotes already defined. 中断,可根本没有哪里定义过这个名字。错误信息的第二行泄了底:Or name \end... illegal, see p.192 of the manual. latex.ltx 里的名字检查先确认该名字未定义,然后 还要求它的前三个字母不是 end,而且名字不是 relax。因为 \end{itemize} 内部是靠调用 \enditemize 实现的,若允许随意创造以 end 开头的名字,环境的配对就会被破坏,所以整个前缀都被保留。「already defined」这句措辞,不过是把两种情形一并囊括的粗略说法。
| 命令 | 遇到已有名称时 | 主要用途 |
|---|---|---|
\newcommand | 报错停止 | 安全地创建新命令 |
\renewcommand | 覆盖它(未定义则报错) | 改写既有命令 |
\providecommand | 什么也不做(保留旧定义) | 可能被重复载入的样式文件 |
\DeclareRobustCommand | 覆盖并写入日志记录 | 用于可动参数的 robust 命令 |
实际分工很清楚。\renewcommand 是「替换 LaTeX 已经提供的东西」这扇门,用 \renewcommand{\labelitemi}{--} 改列表符号就是典型场景。\providecommand 声明的是「若还没有就提供一个」,让自己的样式文件被两处分别载入时也不出事。下例中 \vect 已经存在,所以 \providecommand 什么也不做,粗体的定义得以保留。而 \DeclareRobustCommand 是下一节的主角:它遇到已有名字不会停下,只在 .log 中留下类似 LaTeX Info: Redefining \emph on input line 2. 的一行。记录这次覆盖而不是悄悄执行,正是它与 \renewcommand 的实质区别。
% replace something the class already defines
\renewcommand{\labelitemi}{--}
% define only if nobody else did; here \vect exists, so this line is a no-op
\providecommand{\vect}[1]{\vec{#1}}
% redefine on purpose, and say so in the log
\DeclareRobustCommand{\emph}[1]{\textbf{#1}}宏后面的空格为什么会消失,\xspace 又解决了什么
只由字母构成的命令名 在第一个非字母处结束,而随后的空格会被当作名字的结束标记吃掉。所以在导言区写下 \newcommand{\lab}{Knuth Lab} 后,正文里输入 \lab was founded. 排出来是「Knuth Labwas founded.」。空格并没有凭空消失,而是 TeX 为了判断 \lab 这个名字在哪里截断而吞掉了它。有意思的是还有例外:\$ de 会老老实实保留空格,排成「$ de」。\$ 是由非字母构成的单字符控制符,名字在这一个字符处即告完成,没有理由再往后读。因此这个陷阱 只对由字母拼写的命令名生效。
\usepackage{xspace}
\newcommand{\lab}{Knuth Lab}
\newcommand{\labx}{Knuth Lab\xspace}
\lab was founded. % -> Knuth Labwas founded.
\lab{} was founded. % -> Knuth Lab was founded.
\lab\ was founded. % -> Knuth Lab was founded.
\labx was founded. % -> Knuth Lab was founded.
\labx, and a comma. % -> Knuth Lab, and a comma.补救办法有三种。最常用的是加一对空花括号 \lab{} 来标出名字的结束;其次是控制空格 \lab\ ;第三种是 xspace 宏包提供的 \xspace。\xspace 的巧妙之处在于 不是无条件补空格,而是先看一眼下一个 token 再决定。xspace.sty 里的例外表包含 , . ' / ? ; : ! ~ - ) 和右花括号,以及 \footnote 之类;遇到它们就不补空格。所以 \labx, and 会正确排成「Knuth Lab, and」。该宏包属于 LaTeX 的 tools 套件,原作者是 David Carlisle;需要时可用 \xspaceaddexceptions 扩充例外表。不过要留意代价:\xspace 是一种前瞻技巧,对带参数的宏并无必要(反正以 } 结尾),在别的宏的参数内部偶尔也会给出意外结果。拿不准时,{} 是三者中最稳妥的。
fragile 命令、可动参数、\protect 与 \DeclareRobustCommand
自己写的宏有时会在节标题或图注里突然崩掉,原因就是 可动参数(moving argument)。\section{...} 里的文字不只在正文中排版:它还会为了目录被写入 .aux 辅助文件,并被送去页眉。同一份内容「移动」到了别处。\caption{...}、\thanks{...},以及 tabular 和 array 的 @{...} 也有同样的性质。若某个命令内部的代码在写出的那一刻被展开就会失去意义,就称它为 fragile 命令;能原样被写出而安然无恙的,则称为 robust 命令。
经典对策是 \protect:把它放在 fragile 命令的正前方,告诉 LaTeX「这里不要展开,原样写出去」。一次只保护一个命令。不过有个好消息:自 2019 年 10 月的 LaTeX 发行版起,许多曾经 fragile 的命令已被改造成 robust。 这项改动记录在 LaTeX News 30 的「Making more user commands robust」一节中,甚至连 \begin 与 \end 也一并处理,因此整个环境如今都能写进标题。最顽固的例外是 \verb:把它放进节标题,编译会以 ! LaTeX Error: \verb illegal in argument. 中断(通常还会连带 ! Paragraph ended before \@sect was complete.)。这一个 \protect 救不了,所以在标题或图注里,务实的做法是改写成 \texttt{...}。
% \verb cannot go here at all -- rewrite it
\section{The \texttt{\textbackslash par} primitive}
% a macro that is robust from the start, even though \ifmmode is fragile
\DeclareRobustCommand{\seq}[2][n]{%
\ifmmode #2_{1}\ldots #2_{#1}\else\textbf{??}\fi
}
\section{Sequences $\seq{x}$} % works without \protect对自己写的宏来说,与其每次都记着加 \protect,不如一开始就用 \DeclareRobustCommand 定义为 robust,更为可靠。它接收参数的写法与 \newcommand 完全相同;即便定义体内混有 \ifmmode 这类 fragile 代码,做出来的命令也能挺过可动参数。上面的 \seq 正是 LaTeX 自家文档 clsguide 里的例子,就是为演示这一点而写的。代价只是极轻微的效率损失,所以对那些绝不会出现在标题或图注中的宏,不必一律 robust 化。判断标准只有一条:这个宏有没有可能出现在目录里。
\NewDocumentCommand:现代的定义写法
\newcommand 只能造出一种形状:最多一个方括号可选参数,后面跟必需参数。\NewDocumentCommand{\name}{⟨arg-spec⟩}{...} 拆掉了这个天花板。它不要参数个数,而是接收一段 「参数指定(arg-spec)」:用字母排列出每个参数的类型。它原本是 xparse 宏包的功能,但 2020 年 10 月 1 日的发行版把它并入了 LaTeX 内核(ltcmd 模块),如今无需 \usepackage{xparse} 即可使用。这次并入记录在 LaTeX News 32 中。
| 指定符 | 含义 | 在定义体内如何接收 |
|---|---|---|
m | 必需参数(mandatory) | 普通的 #1 之类 |
o | 可选的 [...] 参数 | 缺失时为「无值」标记 |
O{default} | 带默认值的可选参数 | 缺失时代入 default |
s | 是否带星号 * | 用 \IfBooleanTF 判定 |
这正是它相对 \newcommand 的决定性优势:可以 接收多个可选参数,并把 星号(starred)变体 当作一等公民来处理。写下 s,#1 就会以布尔值的形式告知是否带星号,再用 \IfBooleanTF{#1}{带星}{不带星} 分支即可。把前缀在 New、Renew、Provide、Declare 之间替换,就分别对应 \newcommand、\renewcommand、\providecommand 与无条件覆盖。写新代码时不妨把它当默认选择——不过 \newcommand 既没消失也没过时,一两个参数的短定义照旧使用完全够用。
% s = optional star, m = mandatory argument
\NewDocumentCommand{\diff}{s m}{%
\IfBooleanTF{#1}%
{\frac{\mathrm{d}}{\mathrm{d}#2}}% starred: d/dx
{\mathrm{d}#2}% plain: dx
}
$\diff{x}$ % -> dx
$\diff*{x}$ % -> d/dx
% O{...} gives an optional argument with a default
\NewDocumentCommand{\note}{O{note} m}{\textbf{#1:} #2}宏该怎么命名,定义又该放在哪里
避免命名冲突最好的工具,其实就是 \newcommand 本身。若一上来就用 \renewcommand 或 \def 覆盖,你永远不会知道自己毁掉了什么;先用 \newcommand 定义,一旦出现 already defined,就立刻知道这个名字已有人占用。正因如此,不要随手用 \renewcommand 抹平内核或宏包的命令。给自己的命令起名时,别太短,最好加上项目专用 前缀(如 \myR、\bookTitle)。短的数学算子名尤其早已被占——\ker、\deg、\arg、\Re 都是——所以想用 \R 的话,值得先用 \newcommand 试一次看看。
宏太多同样会让文稿难读。像 \newcommand{\x}{\xi} 这样极端的缩写,对几个月后的自己和合作者而言就是密码。请把宏限定在 重复很多、日后可能批量修改、或值得按含义命名 的对象上,其余照直写出往往更易读。判断标准很简单:这个名字看一眼能懂吗?\vect 能,\x 不能。
最后是放置的位置。单篇论文放在导言区就够了;但若是按章分文件的书稿,或是若干篇共用同一套记号的论文,把定义单独抽成一个文件、用 \usepackage 载入会更好管理。这里还有个小好处:在 .sty 或 .cls 内部,@ 被当作字母对待,因此完全不必写 \makeatletter 就能直接使用 \mybook@vecfont 这样的内部名。由于含 @ 的名字无法从正文调用,你就 在命名上区分开了对外公开的命令与仅供内部使用的命令。若要在导言区做同样的事,就得用 \makeatletter 和 \makeatother 把代码夹起来,多一处容易出错的地方。
% ---- mynotation.sty --------------------------------------------
\ProvidesPackage{mynotation}[2024/01/01 shared notation]
\RequirePackage{amsmath,amssymb}
% private: the @ makes it uncallable from the document body
\newcommand{\mynot@vecfont}[1]{\mathbf{#1}}
% public
\newcommand{\vect}[1]{\mynot@vecfont{#1}}
\newcommand{\R}{\mathbb{R}}
% ---- thesis.tex ------------------------------------------------
% \usepackage{mynotation}