一旦在宏包或导言区写稍微复杂一点的处理,就会需要原始 TeX 做起来很麻烦的东西:布尔标志、“这个命令是否已定义?”的测试、“这个字符串是否为空?”的测试,以及给已有命令追加代码。etoolbox 用接近 LaTeX 的书写方式提供这些工具,pgfkeys 则是构建 key=value 设置接口的基础。许多宏包背后都离不开这两个常用工具。
etoolbox 是什么
etoolbox(Philipp Lehman 编写,现由 Joseph Wright 维护)是面向类和宏包作者的编程工具箱。它给 e-TeX 增加的底层原语套上一层 LaTeX 风格的前端,同时也打包了不少与 e-TeX 不直接相关的通用便利工具。现代 TeX 引擎都包含 e-TeX,因此只要写 \usepackage{etoolbox} 就可以使用。
即使 expl3(LaTeX3 编程层)已经普及,etoolbox 仍然常用,因为它 直接融入 LaTeX2e 的世界。可以自然书写 #1 的参数、熟悉的 {真}{假} 二分支形式,以及能事后修补已有宏包命令的 \patchcmd(后述),都让它在实际导言区工作中非常有用。
布尔标志:toggle 与 bool
当你想保存“是否处于草稿模式”这样的 真/假状态 时,etoolbox 提供两类机制。推荐的是 toggle,它有独立命名空间,因此不会与已有命令冲突。用 \newtoggle{flag} 声明,用 \toggletrue{flag} / \togglefalse{flag}(或 \settoggle{flag}{true})切换,并用 \iftoggle{flag}{⟨true code⟩}{⟨false code⟩} 分支。反向测试可用 \nottoggle{flag}{⟨if not true⟩}{⟨if not false⟩}。
\usepackage{etoolbox}
\newtoggle{draft} % フラグを宣言 / declare the flag
\toggletrue{draft} % 真にする / set it true
% 本文や命令の中で分岐 / branch on it
\iftoggle{draft}
{\textbf{[DRAFT]}\ } % 真のとき / when true
{} % 偽のとき / when false另一类是 bool:\newbool{flag} / \setbool{flag}{true} / \booltrue{flag} / \boolfalse{flag},用 \ifbool{flag}{⟨true⟩}{⟨false⟩} 测试。bool 内部使用与 LaTeX 的 \newif 相同的机制,因此会占用一个命令名 \ifflag。多数时候 toggle 足够;需要与基于 \newif 的代码互操作时,bool 会有用。
| 命令 | 含义 | 备注 |
|---|---|---|
\newtoggle{f} | 声明 toggle f(初始为假) | 有独立命名空间 |
\settoggle{f}{v} | 把 f 设为 v(true/false) | 也可用 \toggletrue / \togglefalse |
\iftoggle{f}{T}{F} | 真则 T,假则 F | 接受三个参数 |
\ifbool{f}{T}{F} | bool 版的分支 | 与 \newif 使用相同机制 |
定义与字符串测试
安全编写宏包时,经常要判断“这个命令是否已经定义?”或“参数是否为空?”。etoolbox 的测试都统一为 {⟨true⟩}{⟨false⟩} 的二分支。定义存在性用 \ifdef{\cmd}{⟨true⟩}{⟨false⟩};按名称(字符串)测试则用 \ifcsdef{name}{⟨true⟩}{⟨false⟩}。反向的 \ifundef / \ifcsundef 也存在。
需要注意的是,名字相似的 \ifdefined 是 e-TeX 的 原语,不是 etoolbox 提供的 {true}{false} 形式命令。需要二分支时,请使用 etoolbox 的 \ifdef(传入带反斜杠的实际命令)。字符串方面,有判断是否只含空白的 \ifblank{⟨string⟩}{⟨true⟩}{⟨false⟩}、其反向 \notblank、比较两个字符串是否相等的 \ifstrequal{⟨string⟩}{⟨string⟩}{⟨true⟩}{⟨false⟩},以及判断空字符串的 \ifstrempty。
% 命令が未定義のときだけ用意する / provide a command only if missing
\ifdef{\highlight}
{} % 既にあれば何もしない / leave it alone
{\newcommand{\highlight}[1]{\textbf{#1}}}
% 引数が空かどうかで出し分け / vary on an empty argument
\newcommand{\field}[1]{\ifblank{#1}{(none)}{#1}}钩子与向命令追加内容
etoolbox 还提供处理 钩子 的命令,也就是在特定时机执行代码的存放点。文档开始/结束时的 \AtBeginDocument / \AtEndDocument 是 LaTeX 内核功能;etoolbox 在此基础上补充了导言区末尾的 \AtEndPreamble、文档真正最后的 \AfterEndDocument,以及围绕特定环境的 \AtBeginEnvironment{⟨env⟩}{⟨code⟩} / \AtEndEnvironment / \BeforeBeginEnvironment / \AfterEndEnvironment。
要在这些钩子或任意宏中 事后添加内容,使用追加系命令。\appto{\cmd}{⟨code⟩} 追加到末尾,\preto{\cmd}{⟨code⟩} 追加到开头(\gappto 是全局版,\eappto 会先展开再追加)。如果要安全地给带参数的宏追加内容,请使用带成功/失败分支的 \apptocmd{\cmd}{⟨code⟩}{⟨success⟩}{⟨failure⟩} / \pretocmd。
% すべての itemize の冒頭に行間設定を差し込む
\AtBeginEnvironment{itemize}{\setlength{\itemsep}{2pt}}
% 文書開始時に走るフックへ追記 / append to the begin-document hook
\appto{\@begindocumenthook}{\typeout{Hello from etoolbox}}用 \patchcmd 修补已有命令
etoolbox 最有名的工具尤其是 \patchcmd。它用于 只替换已经定义好的命令内部的一部分,适合想轻微修改其他宏包或内核命令、但不想整体重新定义的场合。它的格式有五个参数:
\patchcmd{\cmd}{⟨search⟩}{⟨replace⟩}{⟨success⟩}{⟨failure⟩}它在 \cmd 的定义中寻找第一个匹配 ⟨search⟩ 的位置;如果找到,就替换为 ⟨replace⟩ 并执行 ⟨success⟩,如果没找到,则保持命令不变并执行 ⟨failure⟩。只替换第一个出现位置。⟨success⟩ / ⟨failure⟩ 可以留空,但为了能发现补丁失效,失败分支最好放入警告。
实用技巧有两个。第一,目标命令和 ⟨search⟩ 几乎总会包含 @(因为它们是内部命令),所以要么用 \makeatletter … \makeatother 包住,要么利用 \patchcmd 工作时会临时把 @ 当作字母的事实。第二,如果补丁打不上,在导言区放 \tracingpatches,日志会输出“未定义”“模式不匹配”等诊断。若要处理参数指定或替换多处,扩展 etoolbox 的 xpatch 宏包会更方便。
\usepackage{etoolbox}
\makeatletter
% 例:ある内部命令 \@foo の定義中の \small を \footnotesize に差し替える
\patchcmd{\@foo}
{\small} % 探す / search
{\footnotesize} % 置き換える / replace
{} % 成功時 / on success
{\PackageWarning{mypkg}{Patch to \protect\@foo\space failed}} % 失敗時
\makeatother列表与循环
etoolbox 还提供轻量的 内部列表 及其迭代方式。用 \listadd{\mylist}{⟨item⟩} 添加元素,再用 \forlistloop{⟨handler⟩}{\mylist} 对每个元素调用一个单参数处理命令。若要直接遍历手头的逗号分隔字符串,\docsvlist{a,b,c} 和 \forcsvlist{⟨handler⟩}{a,b,c} 很方便。也有 \DeclareListParser 可用自定义分隔符建立解析器。
% カンマ区切りの各要素を箇条書きにする / each CSV item becomes a bullet
\newcommand{\asitem}[1]{\item #1}
\begin{itemize}
\forcsvlist{\asitem}{apples, pears, plums}
\end{itemize}pgfkeys:key=value 接口的基础
pgfkeys 是 PGF/TikZ 内置的强大 键值引擎。TikZ 熟悉的 [draw, thick, fill=blue] 语法,以及许多宏包的 \…setup{...} 风格接口,大多由它实现。核心是 \pgfkeys{/my/key=value}。键用 / 分隔的 路径(family) 组织命名空间,并给每个键分配一个处理器,说明“被调用时要做什么”。
用于 定义键 的处理器是关键。要把值原样存入宏,使用 .store in=\macro(内部为 \def\macro{value})。要用值执行处理,使用 .code={... #1 ...},代码中用 #1 接收传入的值。省略 =value 调用时的默认值由 .default=value 给出,键的初始值由 .initial=value 给出。还可以用 .is choice 枚举选项。
\usepackage{pgfkeys}
% キーを定義する / define keys under /book
\pgfkeys{
/book/title/.store in = \bookTitle, % 値をマクロに格納
/book/edition/.code = {Edition #1}, % #1 は渡された値
/book/edition/.default = 1, % =値 省略時の既定
/book/draft/.initial = false, % 初期値
}
% キーを設定する / set them
\pgfkeys{/book/title=TeX by Topic, /book/edition=3}当宏包提供 \mypkgsetup{...} 这样的入口时,通常会设置 默认路径,避免用户每次都写 /mypkg/ 前缀。惯用写法是 \pgfqkeys{/mypkg}{⟨key list⟩}(q 表示 quick),它是 \pgfkeys{/mypkg/.cd, ⟨key list⟩} 的快速简写。像下面这样写一个一行包装命令,用户就能只用短键名来设置。
% パッケージ側:設定窓口を一行で / the package: a one-line entry point
\newcommand{\mypkgsetup}[1]{\pgfqkeys{/mypkg}{#1}}
% 利用者側:短いキー名で設定 / the user: short key names
\mypkgsetup{title = My Report, edition = 2}LaTeX3 一侧有同类功能 l3keys(expl3 的键值模块),可以用 \keys_define:nn 等声明类似接口。合理分工是:如果用 expl3 写新宏包,就选 l3keys;如果要配合 TikZ/pgf 系代码或既有资产,就选 pgfkeys。