bxgrassator を作っていたら、表題のような基本的な処理で躓いた、という話。
Grass の「擬似 I/O」(文書中で与えられた「入力文字列」から読み TeX コードに書き出す*1)を実装している時に、次のような処理が必要になった。*2
- 無引数マクロ
\InputStr
には「文字列」が格納されている。 - 整数レジスタ
\CharValue
が定義されている。 - 話を簡単にするため、8 ビット欧文 TeX での動作を仮定する。
- 以上の条件のもとで、次の要件を満たすマクロ
\GetCharValue
を作りたい。\GetCharValue
は引数を持たない。\GetCharValue
を実行すると、\InputStr
の先頭にある文字の符号位置を\CharValue
に代入する。- ただし、
\InputStr
が空の場合は何もしない。
以下に挙げるコードは、一見正しく動いているが、実は \GetCharValue
に重大なバグがある。それが何か判るだろうか。ヒントは「\the
-文字列の仕様」である。
%% plain TeX % (ただし \bye 以外は LaTeX でも通用する) \catcode`\@=11 %------------------------ %% \SetInputStr{<トークン列>} % 入力のトークン列を「文字列化」して \InputStr に入れる。 \def\SetInputStr#1{% \def\xx@x{#1}% 一旦マクロに格納 \expandafter\xx@set@input@str@a\meaning\xx@x\@nil % \meaning を適用する } % \meaning の展開結果は「macros:->(本体)」という the-文字列になる \def\xx@set@input@str@a#1>#2\@nil{% 「>」をスキャン \def\InputStr{#2}% 「>」から後を \InputStr に格納する } %% \CharValue: \GetCharValue の結果が返る。整数レジスタ。 \newcount\CharValue %% \GetCharValue % \InputStr が非空ならば先頭文字の符号値を \CharValue に代入する。 \def\GetCharValue{% \ifx\InputStr\empty\else % \InputStr が非空ならば \expandafter\xx@get@char@value@a \fi } \def\xx@get@char@value@a{% \expandafter\xx@get@char@value@b\InputStr\@nil } \def\xx@get@char@value@b#1#2\@nil{% \CharValue=`#1\relax % 先頭文字の符号値を \CHarValue に代入 \def\InputStr{#2}% \InputStr を更新 } \catcode`\@=12 %------------------------ %% テスト。入力を受け取って、\GetCharValue を 3 回呼ぶ。 \def\Test#1{% \SetInputStr{#1}% \TestOne \TestOne \TestOne } \def\TestOne{% \CharValue=-1 % \GetCharValue \immediate\write16{value=\the\CharValue}% } % これらは問題ないのだが… \Test{\TeX} % 92 84 101 \Test{?} % 63 -1 -1 \bye
正しく動かない例。
\Test{A B} % 65 66 -1 % 正しくは 65 32 66 となるべき
要するに、空白文字が読み飛ばされているのである。\meaning
や \string
で出力されるトークン列は、ほぼ全ての文字が「非特殊な記号」(カテゴリコード 12)となるが、唯一の例外で空白文字は「空白」としての性質(カテゴリコード 10)を保つのである。だから、単純に区切り無し引数のマクロを用いると「通常通り」空白文字は飛ばされてしまう、というわけである。
ここまで読んで、「じゃあどうすればいいの?」と疑問に思った人は、しばらく自分で考えてもらいたい。気が向いたら、後日解説するかも知れない。少なくとも、bxgrassator のソースに答えがあることは確かである。((bxgrassator.def の \bxgs@get@char
マクロ。))