プログラミング補助(etoolbox / pgfkeys)

Philipp Lehman は biblatexcsquotes の作者として知られていますが、いちばん多くのプリアンブルで静かに動いているのは彼の三つ目の作品 etoolbox でしょう。理由はほぼ一つの命令にあります——\patchcmd、他人のマクロを丸ごと再定義せずに 一部だけ差し替える 道具です。ただしこれには落とし穴があって、探索文字列が見つからなくても \patchcmdエラーも警告も出さずに黙って何もしません。パッケージが更新された翌日にプリアンブルの調整が「なぜか効かなくなる」のは、たいていこれです。このページでは etoolbox の判定・フラグ・フック・パッチと、LaTeX で key=value インターフェースを組み立てる pgfkeys、そして小数計算の \fpeval を扱います。

etoolbox とは何か — e-TeX の道具箱に LaTeX の顔をかぶせたもの

etoolbox は、クラスやパッケージを書く人のためのプログラミング道具箱 です。e-TeX が追加した低水準のプリミティブを LaTeX2e らしい書き味 に包み直し、そこへ汎用の便利道具を大量に足したもの。TeX Live 2024 に入っているのはバージョン 2.5k(2020 年 10 月 5 日)で、著作権表示には Philipp Lehman(2007–2011)と Joseph Wright(2015–2020)の二人が並びます。現代の TeX エンジンはすべて e-TeX を内蔵しているので、\usepackage{etoolbox} と書けばそのまま使えます。expl3(LaTeX3 のプログラミング層)が広まったいまも etoolbox が生き残っているのは、LaTeX2e の世界にそのまま馴染むから です。引数は素直に #1 と書け、分岐は見慣れた {真}{偽} の二択で、しかも他人のパッケージを後から直す \patchcmd がある——プリアンブルの実務にはこれが効きます。

判定を書く — \ifdef\ifdefempty\ifstrequal

etoolbox の判定はすべて同じ形をしています——{⟨真のときのコード⟩}{⟨偽のときのコード⟩} の二択を最後に取る\fi を書く必要も、\else の位置に悩む必要もありません。「この命令はもう定義されているか」は \ifdef{\cmd}{真}{偽}、名前の文字列で調べるなら \ifcsdef{name}{真}{偽}(否定形の \ifundef\ifcsundef もあります)。文字列については、空白だけかを見る \ifblank、その否定の \notblank、二つの文字列の一致を見る \ifstrequal{文字列}{文字列}{真}{偽}、マクロの中身が空かを見る \ifdefempty{\cmd}{真}{偽}、そして与えた文字列そのものが空かを見る \ifstrempty{文字列}{真}{偽} が使えます。名前の似た \ifdefined は e-TeX のプリミティブ であって etoolbox の二択形ではないので、混同しないでください。

latex
\usepackage{etoolbox}

% provide a command only if nobody defined it yet
\ifdef{\highlight}
  {}                                    % already there: leave it alone
  {\newcommand{\highlight}[1]{\textbf{#1}}}

% behave differently on an empty argument
\newcommand{\field}[1]{\ifblank{#1}{(none)}{#1}}

% numeric tests, same two-way shape
\ifnumcomp{\value{page}}{>}{10}{late}{early}
\ifnumodd{\value{page}}{recto}{verso}

ここに一つ、ドキュメントを読んでいても気づきにくい違いがあります。\ifstrequal\ifdefstring は展開可能ではありません。 etoolbox のソースを見ると、この二つは \newrobustcmd で定義されている——つまり e-TeX の \protected が付いている——ので、\edef\typeout\csname の中では期待どおりに動きません。実際に \typeout{\ifstrequal{abc}{abc}{SAME}{DIFF}} と書くとログには SAME ではなく \ifstrequal {abc}{abc}{SAME}{DIFF} がそのまま出ます。一方 \ifdefempty は展開可能で、\edef に入れると期待どおり結果だけが残ります。本文中で分岐するぶんにはどれも問題なく、\edef に入れたときだけ差が出る ——この線引きを覚えておくと、原因不明の「判定が効かない」に一日溶かさずに済みます。

真偽フラグ — \newtoggle\newbool はどちらを使うか

既定で選ぶべきは \newtoggle です。理由は名前空間にあります。toggle は独自の名前空間を持つので、既存の命令とぶつかりません。宣言は \newtoggle{draft}、切り替えは \toggletrue{draft}\togglefalse{draft}(あるいは \settoggle{draft}{true})、分岐は \iftoggle{draft}{⟨真⟩}{⟨偽⟩}、否定は \nottoggle です。もう一方の bool\newbool{draft}\setbool{draft}{true}\booltrue\boolfalse\ifbool{draft}{⟨真⟩}{⟨偽⟩} という同型の一式ですが、内部では LaTeX 標準の \newif と同じ仕掛けを使うため \ifdraft という命令名を一つ消費します。ここが判断の分かれ目——\newif で書かれた既存コードと相互運用したいときだけ bool を選び、それ以外は toggle で十分です。

命令意味注意
\newtoggle{f}フラグ f を宣言(初期値は偽)独自の名前空間。命令名を消費しません
\settoggle{f}{v}f を v(true / false)にする\toggletrue / \togglefalse でも同じ
\iftoggle{f}{T}{F}真なら T、偽なら F引数は三つ。\fi は不要
\newbool{f}bool 版のフラグ宣言\newif と同じ仕掛け。\iff 形式の命令名を一つ使います
\ifbool{f}{T}{F}bool 版の二択分岐\newif で書かれた既存コードと混ぜられます

\newrobustcmd\robustify — 壊れないマクロを作る

\newrobustcmd\newcommand とまったく同じ書き方で、頑健(robust)な命令を作ります。 違いは \meaning で一目瞭然です——\newcommand で作った命令は \long macro:->… ですが、\newrobustcmd で作ると \protected\long macro:->… になります。つまり LaTeX 伝統の \protect を使った二段構えではなく、e-TeX の \protected プレフィックスを直接使う。だから見出しやキャプションのような「動く引数」に入れても、目次ファイルへ書き出される途中で勝手に展開されて壊れることがありません。すでに他人が定義してしまった脆い命令には \robustify{\cmd} が使えて、定義済みの命令をその場で頑健化します。

\patchcmd — 他人のマクロを一部だけ書き換える

\patchcmd は、すでに定義済みのマクロの本体から探索文字列を見つけ、そこだけを差し替えます。 引数は五つ——\patchcmd{\cmd}{⟨探す⟩}{⟨置き換える⟩}{⟨成功時⟩}{⟨失敗時⟩} です。見つかれば置換して四つ目を、見つからなければマクロに一切触れずに五つ目を実行します。置き換えられるのは 最初の一つだけ で、本体に \small が二つあれば手前の一つしか変わりません。実際に役に立つ例を挙げます。article クラスの thebibliography 環境は \section*{\refname} で始まるので、その \section*\section に差し替えれば 参考文献が番号付きの節になり、目次にも載ります。実測でも .toc\contentsline {section}{\numberline {2}References} が書き出され、狙いどおりに動きました。

document.tex
\usepackage{etoolbox}

\makeatletter                    % the target usually contains @
\patchcmd{\thebibliography}
  {\section*}                    % search
  {\section}                     % replace
  {\typeout{bibliography patch applied}}                        % on success
  {\PackageWarning{mypkg}{bibliography patch failed}}           % on failure
\makeatother

% result: "References" becomes a numbered section and enters the ToC
%   .toc -> \contentsline {section}{\numberline {2}References}{1}{}

パッチが黙って効かないとき — \tracingpatchesxpatch

\patchcmd の失敗は 完全に無音 です。実測してみると、当たらないパターンを渡して成功・失敗の両分岐を空にした場合、コンパイルは エラー 0 件・警告 0 件 で通り、ログにも痕跡が残りません。だから鉄則は一つ——失敗分岐は絶対に空にせず、\PackageWarning を置く。そうすれば Package mypkg Warning: bibliography patch failed on input line 5. が出て、更新の翌日にすぐ気づけます。原因を突き止めるには \tracingpatches をプリアンブルに置いてください。etoolbox.def が読み込まれ、パッチごとに診断がログへ書き出されます。

log
[debug] tracing \patchcmd on input line 5
[debug] analyzing '\thebibliography'
[debug] ++ control sequence is defined
[debug] ++ control sequence is a macro
[debug] ++ macro can be retokenized cleanly
[debug] -- search pattern not found in replacement text

[debug] analyzing '\nosuchcommand'
[debug] -- control sequence is undefined or \relax

[debug] analyzing '\LaTeX'
[debug] -- macro cannot be retokenized cleanly
[debug] -> the macro may have been defined under a category
[debug]    code regime different from the current one

診断は三種類に分かれます。「探索パターンが本体に無い」-- search pattern not found in replacement text)はパッケージが更新されて中身が変わった典型で、新しい定義を \show で確認して探索文字列を書き直します。「命令が未定義」-- control sequence is undefined or \relax)はパッチを当てる位置が早すぎるので、\AtBeginDocument などで後ろへ回します。三つ目の 「再トークン化できない」-- macro cannot be retokenized cleanly)はカテゴリコードの問題で、そのマクロが現在と違う catcode 環境下で定義されたことを意味します——\makeatletter の内側で当てているか確かめてください。

そしてもう一つ、診断にすら現れない失敗があります。\patchcmd は、オプション引数を持つ命令には効きません。 \newcommand{\opt}[2][X]{...} と定義した \opt の中身を \meaning で見ると \@protected@testopt \opt \\opt {X} ——つまり \opt は分岐するだけの入口で、本体は \\opt という別の命令に入っています。だから \patchcmd{\opt}{small}{LARGE} は入口を探して失敗します。この場合は etoolbox を拡張した xpatch パッケージ\xpatchcmd を使ってください——実測では同じ引数で成功し、内部マクロが \long macro:[#1]#2-><#1|#2|LARGE> に書き換わりました。xpatch は環境用の \xpatchcmd 系統も揃えています。

フック・追記・リスト — 既存のコードに割り込む

マクロ本体を書き換えずに済むなら、そのほうが安全です。etoolbox は「特定の瞬間に走るコードを追加する」ためのフックを豊富に用意しています。 文書の開始・終了は LaTeX カーネルの \AtBeginDocument\AtEndDocument が担当しますが、etoolbox はプリアンブルの最後で走る \AtEndPreamble、本当に最後の \AfterEndDocument、そして特定の環境の前後に割り込む \AtBeginEnvironment{⟨env⟩}{⟨コード⟩}\AtEndEnvironment\BeforeBeginEnvironment\AfterEndEnvironment を足します。既存のマクロやフックへ 後から書き足す なら \appto{\cmd}{⟨コード⟩}(末尾へ)と \preto{\cmd}{⟨コード⟩}(先頭へ)——大域版が \gappto、追加するコードを先に展開する版が \eappto です。引数をとるマクロには成功・失敗分岐付きの \apptocmd\pretocmd を使います——こちらも未定義の命令に対しては失敗分岐が走るだけでエラーは出ないので、\patchcmd と同じ注意が要ります。

latex
\usepackage{etoolbox}

% run code every time an environment starts -- no patching required
\AtBeginEnvironment{quote}{\itshape}
\AtBeginEnvironment{itemize}{\setlength{\itemsep}{2pt}}

% append to a macro that takes an argument (note the two branches)
\newcommand{\greet}[1]{Hello #1}
\apptocmd{\greet}{!}{}{\PackageWarning{mypkg}{could not extend \string\greet}}
% \greet is now  \long macro:#1->Hello #1!

% lightweight lists and loops
\listadd{\mylist}{alpha}\listadd{\mylist}{beta}
\newcommand{\asitem}[1]{\item #1}
\begin{itemize}\forlistloop{\asitem}{\mylist}\end{itemize}
\begin{itemize}\forcsvlist{\asitem}{apples, pears, plums}\end{itemize}

リスト関連も一通り揃っています。\listadd{\mylist}{⟨要素⟩} で内部リストに追加し、\forlistloop{⟨ハンドラ⟩}{\mylist} で各要素に一引数のハンドラを適用します。手元にカンマ区切りの文字列があるなら \docsvlist{a,b,c}\forcsvlist{⟨ハンドラ⟩}{a,b,c} が手軽です。区切り文字を自分で決めたければ \DeclareListParser でパーサを作れます。実務では、パッケージのオプションを受け取ってリストとして回す、という使い方が多いでしょう。

pgfkeyskey=value のインターフェースを自分の道具に付ける

pgfkeys は PGF/TikZ に同梱されている key=value エンジン で、TikZ でおなじみの [draw, thick, fill=blue] という書き味も、多くのパッケージの \…setup{...} 型インターフェースも、その大半がこれで実装されています(TeX Live 2024 の PGF はバージョン 3.1.10、著作権は Till Tantau)。中心にあるのは \pgfkeys{/my/key=value} のただ一つの命令です。キーは / で区切られた パス(ファミリー) で名前空間を分け、各キーには「呼ばれたら何をするか」を決めるハンドラを割り当てます。要するに、キーの定義とはハンドラの選択のことです。

.store in.code.is choice — ハンドラの使い分け

値をそのまま保管したいなら .store in=\macro値を使って何かを実行したいなら .code={... #1 ...}(渡された値が #1 に来ます)、選択肢を列挙したいなら .is choice ——この三つで実用の大半は足ります。加えて =値 を省いて呼ばれたときの値を .default=値 で、キーの初期値を .initial=値 で与えられます(初期値は \pgfkeysvalueof{/path/key} で読み出せます)。パッケージ側で \mypkgsetup{...} のような窓口を出すなら \pgfqkeys{/mypkg}{⟨キー列⟩} が定石です——q は quick の意味で、\pgfkeys{/mypkg/.cd, ⟨キー列⟩} の短縮形。これを一行のラッパにしておけば、利用者は短いキー名だけで設定できます。

document.tex
\usepackage{pgfkeys}

\pgfkeys{
  /book/title/.store in    = \bookTitle,
  /book/edition/.store in  = \bookEd,
  /book/edition/.default   = 1,          % value used when called bare
  /book/pages/.initial     = 100,        % starting value
  /book/layout/.is choice,               % a fixed set of options
  /book/layout/wide/.code   = {\def\bookLayout{WIDE}},
  /book/layout/narrow/.code = {\def\bookLayout{NARROW}},
  /book/note/.code = {\def\bookNote{<<#1>>}},   % #1 is the value passed in
}

\pgfkeys{/book/title=TeX by Topic, /book/edition, /book/layout=wide}
\pgfkeysvalueof{/book/pages}          % -> 100

% a one-line entry point for your users
\newcommand{\mypkgsetup}[1]{\pgfqkeys{/book}{#1}}
\mypkgsetup{title = My Report, edition = 2}

pgfkeys のエラーメッセージは親切で、検索の手がかりにもなります。定義していないキーを渡すと ! Package pgfkeys Error: I do not know the key '/book/nosuchkey', to which you passed '1', and I am going to ignore it. Perhaps you misspelled it..is choice に無い選択肢を渡すと ! Package pgfkeys Error: Choice 'sideways' unknown in choice key '/book/layout'. I am going to ignore this key. が出ます。どちらも 「無視して続行する」 ——つまり組版は止まらないので、ログを読まないとキーの綴り間違いに気づけません。なお LaTeX3 側には同等の l3keys\keys_define:nn など)があります。使い分けの目安は、expl3 で新しいパッケージを書くなら l3keys、TikZ 由来のコードや既存資産に合わせるなら pgfkeys です。

小数の計算 — \fpeval はもう xfp を読まなくていい

TeX の整数演算では、\numexpr の割り算が四捨五入されてしまうなど、小数を扱う場面で無理が出ます。そこで \fpeval です——\fpeval{1/3}0.3333333333333333\fpeval{sqrt(2)}1.414213562373095\fpeval{sind(30)}0.5\fpeval{round(2/3, 4)}0.6667 を返します。長さと組み合わせたければ \setlength{\x}{\fpeval{345/7}pt} のように単位を後ろに付けるだけです。ここで一つ、時点を明示して書いておきます——TeX Live 2024 同梱の LaTeX2e(2023-11-01 版)では、\fpeval\inteval\dimeval はカーネルに入っており、\usepackage{xfp} は不要です。 xfp 側も \ProvideExpandableDocumentCommand で「無ければ用意する」形になっているので、読み込んでも害はありません。古い環境も相手にするなら読み込んでおくのが無難です。

最後に、この三つの道具をどう組み合わせるかの目安を。プリアンブルで他人の振る舞いを少しだけ変えたいなら etoolbox(そして失敗分岐に必ず警告を置く)。自作パッケージに設定インターフェースを付けたいなら pgfkeysl3keys寸法や比率を計算したいなら \fpeval。そして常に最初に問うべきは「そもそもパッチを当てずに済む方法はないか」です。公開命令の \renewcommand、フックへの \AtBeginEnvironment、正規のパッケージオプション——この順に検討して、どれも無理なときに初めて \patchcmd を抜いてください。パッチは今日は動いても、明日のパッケージ更新までしか保証がありません。