[LaTeX2ε] keyvalパッケージで予約語–値形式のパッケージオプションを処理する

本稿では，予約語–値 (key–value) 形式のパッケージオプションを，keyvalパッケージを用いて処理する方法を提示する。

## 動機
LaTeX2εで予約語–値形式の文字列を処理する際は，[keyvalパッケージ](https://www.ctan.org/pkg/keyval)を用いるのが通例であろう。しかしながら，keyvalパッケージは専ら命令の引数を処理することを目的としている為，予約語–値形式のパッケージオプションの処理には援用し難い。

予約語–値形式のパッケージオプションを処理したいという要望はある程度存在する様であり，既に[xkeyval](https://www.ctan.org/pkg/xkeyval)，[options](https://www.ctan.org/pkg/options)等数点のパッケージが存在する。従って本稿の内容は「車輪の再発明」との罵りを受けても仕方無いことを行なっている訳であるが，個人的に，LaTeX3事業公式が直接管理している標準範囲内において，当目的を達成したかった。

## 方法と実装
### keyvalパッケージの基本的な使用法
先ず，keyvalパッケージで予約語–値形式を扱う尋常の方法を説明する。keyvalパッケージの解説を目的としたものではない為，網羅的に解説することも使用上の注意を述べることもしない。

この例ではlocal-x.styというパッケージファイルを作成する。その中で，`\somecmd`という命令の第1引数をkeyvalパッケージを用いて処理する。

一連の予約語–値形式を扱うに当たって，始めに一つの族 (family) を自分で決定する。典型的には，そのオプションを処理する対象の命令の名前とパッケージの名前空間を組み合わせるなどするのが善いだろう。この例では名前空間を`lclx`とし，従って`\somecmd`命令に対する族は「`lclx@@somecmd@opt`」とする。

次に，その族に属する予約語を宣言していく。宣言の際，値の既定値を任意で定めてもよい。なお，「値が必須でない」つまり単に予約語の名前を書くだけで良いような類の予約語を宣言する場合は，値の既定値を空として明示しなくてはならない。そうしないと，いざ予約語だけを指定した場合に，「予約語に対する値が指定されていない」という旨の警告が生じてしまう。
この例では，`xxx`という宣言型の予約語と`yyy`・`zzz`という値を取る予約語の計3つを宣言する。

```latex
\define@key{lcl@@somecmd@opt}{xxx}[]{%
\typeout{xxx!!!}}
\define@key{lcl@@somecmd@opt}{yyy}{%
\typeout{yyy is #1!!!}}
\define@key{lcl@@somecmd@opt}{zzz}{%
\typeout{z3 is \string #1!!!}}
```

予約語を宣言する`\define@key`の第3必須引数は，当該予約語が宣言された時に行なう動作を記述する。利用者が指定した値は，`#1`という引数文字列を介して扱うことができる。実地では，ここでの動作は組版の出力というより寧ろ真偽値・文字列値の設定・変更が主であり，その後設定・変更した値に基づいて命令の本格的な処理に進む訳であるが，例示を完結・明瞭にする為に，ここでは端末（及び記録用紙）に即座に書き出すような設定にした。

続いて`\somecmd`命令の宣言を行なう（実のところ，予約語の宣言と命令の宣言は順不同である）。`\somecmd`に指定された引数を処理したいのだから，`\somecmd`の命令定義の内部で，keyvalパッケージが提供する`\setkey`命令を用いることになる。`\setkey`命令は指定した族に属する（`\define@key`で宣言した）予約語を順次処理していく。

```latex
\newcommand*\somecmd[1]{%
\setkeys{lcl@@somecmd@opt}{#1}%
}
```

当然，`\somecmd`の定義はもっと複雑であっても良く，「`\setkeys`命令のみ含めることができる」という訳では決してない。更に言えば，`\setkeys`命令は単に族と予約語を見ているだけなので，命令定義の内部専用という訳でもない。

こうした一連の設定の後，`\somecmd`を実際に実行してみる。

```latex
\somecmd{xxx, yyy = aaa, zzz = \today}
```

すると，端末に

```
xxx!!!
yyy is aaa!!!
z3 is \today!!!
```

という具合の文章が表示される筈だ。keyvalパッケージの凡その機能はこれで把握できるだろう。

### パッケージオプション
LaTeX2εでは，パッケージが読み込まれる際に`\@pass@ptions`という命令を用いて，`\opt@〈パッケージ名前〉.sty`[^cs]という命令にオプションを格納する。local-xパッケージならば`\opt@local-x.sty`という命令に，`\usepackage`の第1任意引数に指定したオプション文字列が格納されている。

たったこれだけ判明すれば，あとは実装するだけである。`\define@key`で予約語を宣言しておいて，`\setkey`命令に`\opt@〈パッケージ名前〉.sty`命令の内容を渡してやればよい。

だたし，

* LaTeX2εが標準で提供しているパッケージオプション処理`\ProcessOptions`は，内部に`\protect`命令（等？）を含むオプション文字列を処理すると何かおかしなこと（精査していない）が起きて異常終了するので，`\ProcessOptions`に到達する以前にパッケージオプション文字列を手動で空にする
* `\setkey`命令には，予約語–値形式の文字列を文字列として展開してから渡す必要がある

ことに対処した為，少々複雑に見えるかもしれない。要の処理は至極単純である。

```latex:local-x.sty
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{local-x}%
[2020-07-01 0.1.0 User local package file.]
\expandafter\def
\expandafter\lclx@@opt@list
\expandafter{%
\romannumeral-`>%
\@ptionlist{\@currname.\@currext}}
\DeclareOption*{}
\expandafter\let
\csname
opt@\@currname.\@currext
\endcsname
=\@empty
\ProcessOptions
\RequirePackage{keyval}
\define@key{\@currname}{xxx}[]{%
\typeout{xxx!!!}}
\define@key{\@currname}{yyy}[bbb]{%
\typeout{yyy is #1!!!}}
\define@key{\@currname}{zzz}[bbb]{%
\typeout{z3 is \string #1!!!}}
\expandafter\setkeys
\expandafter{%
\expandafter\@currname
\expandafter}%
\expandafter{%
\lclx@@opt@list}
\endinput
```

```latex:main.tex
%&uplatex-dev
\documentclass[dvipdfmx]{ujarticle}
\usepackage[xxx, yyy = aaa, zzz = \date]{local-x}
\begin{document}
a
\end{document}
```

### 問題点

* keyvalパッケージで命令の引数を処理する場合，制御綴は制御綴として渡される様である（詳細は分からなかった）。例えば`\somecmd`の例において，予約語`zzz`に対する`\today`という値は，`\string`を前置することで「\today」という出力が得られる。一方パッケージオプションの場合，`\opt@〈パッケージ名前〉.sty`命令にオプション文字列が格納される段階で「\today」が展開されてしまう為，上掲の例では`zzz`に対する値に`\string`を前置すると「2020\penalty\@M 年7\penalty\@M 月1\penalty\@M 日」という出力に成り，両者の出力結果が食い違う。この挙動は問題であるが，ずっと高性能である筈のxkeyvalパッケージにおいても同様の現象が見受けられる為，解決を諦めている。

* keyvalパッケージで命令の引数を処理する場合，波括弧で囲んだ値は内部の等号・句点がそのまま予約語に対する値として渡される。しかし，これも`\opt@〈パッケージ名前〉.sty`命令にオプション文字列が格納される段階で先んじて処理されてしまう為，句点を含む文字列を波括弧で囲った場合，期待に反する異常な値が生じることになる。この挙動はかなり深刻な問題であるが，ずっと高性能である筈のxkeyvalパッケージにおいても同様の現象が見受けられる為，解決を諦めている。

## 参考
* [The LaTeX2ε Source](https://www.latex-project.org/help/documentation/source2e.pdf) 2019-10-01 Patch level 1
* [The `keyval` Package](http://mirrors.ctan.org/macros/latex/required/graphics/keyval.pdf) 2014-10-28 v1.15

[^cs]: 説明の便宜上，命令を「逆斜線を前置した一連の文字列」と表記したが，読点や（パッケージ名に含まれる可能性のある）連字符は，英字の様に連続した一つ命令の名前として登録することは通常できない（パッケージ読み込み時の各々の範疇符号が英字の範疇符号と異なる）。実際には`\csname`と`\endcsname`で囲むなどして，読点・連字符を含む文字列を一続きの命令名として宣言できるようにしている。