マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

\write18 よりヤバい \write16 の話

前の記事では「新しい LuaTeX で \write18 できない話」をした。新しい(0.85 版以降)LuaTeX で「\write18 でシェル実行を行う」という文法が廃止されたのにはそれなりの理由がある。

なぜ \write18 できないのか

それは、「出力ストリーム(\openout)の個数を従来の 16 から 128 に増長した」ことである。つまり新しい LuaTeX では次のコードが通るようになる。

\openout18=hoge.txt

そうすると、これを実行した後に次のコードを実行するとどうなるか。

\immediate\write18{rm thesis-slide.tex}

当然、「rm thesis-slide.tex」という文字列がファイルに hoge.txt に書き出されるべきであろう。18 が有効なストリーム番号である以上、18 を特別扱いできないのである。

LaTeX の \newwrite

ところで、これも既に述べたように、現在の LaTeX カーネルでは「必要な範囲で拡張エンジンの仕様に合わせる」という方針になっている。従って、LuaTeX で出力ストリームの個数が増えたのに応じて、LaTeX カーネル\newwrite 命令の定義も変更されている。

\ifx\directlua\@undefined % LuaTeX以外
  \def\newwrite   {\e@alloc\write \chardef{\count17}\m@ne\sixt@@n}
\else                     % LuaTeXの場合
  \def\newwrite   {\e@alloc\write
                   {\ifnum\allocationnumber=18 \allocationnumber19\fi %(*)
                     \global\chardef}%
                   {\count17}%
                   \m@ne
                   {128}}% ←これが上限値
\fi

詳しい説明は省くが、「128」が出てきていることは判るだろう。さらに、(*) の行をみると「18 をスキップしている」ということも推測できる。前の記事で述べたように、shellesc では「\write18 がこれまで通り動く」仕掛けを施しているため、18 が実際に使われると不都合である。それを回避しているのであろう。

\write16 がヤバい

これを見ると、“\write18 の問題”については対策が済んでいることがわかる。ところが、新しい LuaTeX の出力ストリームの仕様に関して、対策ができていない(ように見える)問題がある。それは \write16 である。

一般の TeX 言語者にとっては既知だと思われるが、TeX 言語において「端末に文字列を表示する」ためには \write16 を使うのが通例である。詳しくいうと、次の通りである。TeX 言語の仕様では、「“無効または非オープン”の非負の値の出力ストリーム」は端末(標準出力)とログの両方を対象としている。そして出力ストリームが 0〜15 しかない従来のエンジンでは 16 以上は常に無効である。従って、慣習的に 16 が「端末(とログ)への出力」として用いられているのである。

ところが新しい LuaTeX で 0〜127 が有効な番号になってしまうと、先程 \write18 についての議論と同様に、\write16 が「端末に出力される」ことも保証されない。といっても、非オープンのストリームは結局端末として扱われるので、番号 16 が実際にオープンされるまでは従来の動作と変わらない。しかし、先の LaTeX\newwrite のコードでは 18 はスキップしているのに 16 はスキップしていない。だから、結局 LuaLaTeX では \write16 が「端末に出力される」ことは保証されないのである。

LaTeX 自体は問題なし、だけど…

果たしてこれらの事実はどう解釈すればいいのだろうか? LaTeX で端末に出力したい場合に \write16 を使うのはマチガイなのだろうか?

これを考えるにあたってまず気になるのがLaTeX カーネル自体はどうしているのか」である。LaTeXカーネルLaTeX での文書コンパイル中に端末にメッセージを表示していて、それには当然 \write16 を使っているはず……。

LaTeX で「端末に表示」するためのユーザ命令 \typeout の定義を調べてみよう。((これは通常の「文書コンパイル時の \typeout の定義」である。フォーマット作成中は別の定義が使われる場合もある。))

\def\typeout#1{\begingroup\set@display@protect
    \immediate\write\@unused{#1}\endgroup}

この他、警告を出すための \@latex@warning\@latex@warning@no@line は処理を \GenericWarning に移譲しているが、それの定義は次のようである。

[latex.ltx;抜粋]
\DeclareRobustCommand{\GenericWarning}[2]{%
   \begingroup
      \def\MessageBreak{^^J#1}%
      \set@display@protect
      \immediate\write\@unused{^^J#2\on@line.^^J}%
   \endgroup
}

これらを見ると、出力先のストリーム番号は「16」ではなく「\@unused」となっている。そしてこの番号は普通に \newwrite で割り当てられている。

\newwrite\@unused

\@unused という名前が示すように、この番号(具体的な値は 0 である)のストリームに対しては(少なくとも LaTeX カーネルでは)決してオープンをしていない。そのため端末に出るという仕組である。要するに、LaTeX が行っている端末出力は \write16 ではなく \write\@unused である((なお、LaTeX のフォーマット作成時(まだ\newwrite を定義する前の段階)では端末出力のための番号には 16、および(何故か)17 が使われている。))ため、たとえ 16 が有効な番号になっても影響を受けないのである。

つまり、\newwrite が 16 をスキップしないのは、“LaTeX としては”正当化できる。LaTeX で端末に出力したい場合は、\typeout などの LaTeX が用意した命令を使うか、そうしないなら、\write16 ではなく \write\@unused としなければいけなかった、ということになる。

……とはいっても、TeX 言語では「端末出力は \write16」が常識であるし、LaTeX でもそうするのが寧ろ普通であっただろう。現実に、TeX Live にあるパッケージでも \write16 を使っている例は多数見つかる。

[lisp-prim.sty:105行目]
  \toks0\expandafter{\@temp@write@buffer}%
  \immediate\write16{\the\toks0}%
  \let\@temp@write@buffer\@save@write@buffer}

果たして、TeX on LaTeX な開発者は、\write18 だけでなくて \write16 も修正しなければいけないのだろうか……?