编程辅助工具

一旦在宏包或导言区写稍微复杂一点的处理,就会需要原始 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⟩}

latex
\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

latex
% 命令が未定義のときだけ用意する / 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

latex
% すべての itemize の冒頭に行間設定を差し込む
\AtBeginEnvironment{itemize}{\setlength{\itemsep}{2pt}}

% 文書開始時に走るフックへ追記 / append to the begin-document hook
\appto{\@begindocumenthook}{\typeout{Hello from etoolbox}}

用 \patchcmd 修补已有命令

etoolbox 最有名的工具尤其是 \patchcmd。它用于 只替换已经定义好的命令内部的一部分,适合想轻微修改其他宏包或内核命令、但不想整体重新定义的场合。它的格式有五个参数:

latex
\patchcmd{\cmd}{⟨search⟩}{⟨replace⟩}{⟨success⟩}{⟨failure⟩}

它在 \cmd 的定义中寻找第一个匹配 ⟨search⟩ 的位置;如果找到,就替换为 ⟨replace⟩ 并执行 ⟨success⟩,如果没找到,则保持命令不变并执行 ⟨failure⟩。只替换第一个出现位置。⟨success⟩ / ⟨failure⟩ 可以留空,但为了能发现补丁失效,失败分支最好放入警告。

实用技巧有两个。第一,目标命令和 ⟨search⟩ 几乎总会包含 @(因为它们是内部命令),所以要么用 \makeatletter\makeatother 包住,要么利用 \patchcmd 工作时会临时把 @ 当作字母的事实。第二,如果补丁打不上,在导言区放 \tracingpatches,日志会输出“未定义”“模式不匹配”等诊断。若要处理参数指定或替换多处,扩展 etoolboxxpatch 宏包会更方便。

document.tex
\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 可用自定义分隔符建立解析器。

latex
% カンマ区切りの各要素を箇条書きにする / 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 枚举选项。

latex
\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⟩} 的快速简写。像下面这样写一个一行包装命令,用户就能只用短键名来设置。

latex
% パッケージ側:設定窓口を一行で / 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