マクロツイーター

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

新しい LuaTeX だって \write18 したい

前の記事で触れたように、新しい(0.85 版以降の)LuaTeX では「\write18 でシェル実行(外部コマンドの実行)する」ことができなくなっている。

% 残念ながら新しいLuaTeXでは動作しない
\immediate\write18{rm thesis-slide.tex}

じゃあ新しい LuaTeX でシェル実行したい場合はどうすればよいか、という話。

Lua でシェル実行しよう

LuaTeX なので当然 Lua ができる。そして、Lua にはシェル実行のための関数 os.execute() が存在する。だから \write18 の代わりに次のように書けばよい。

% \immediate\write18{rm thesis-slide.tex}
% の代わりにコレ
\directlua{ os.execute('rm thesis-slide.tex') }

もし、シェルの“遅延実行”、すなわち \immediate でない \write18 が必要な場合は、\directlua でなく \latelua を用いる。

% \write18{rm thesis-slide.tex}
% の代わりにコレ
\latelua{ os.execute('rm thesis-slide.tex') }

もちろん、Lua はイロイロできるので、外部コマンドを使わないという手も考えられる。

% これだとどんなOSでも大丈夫
\directlua{ os.remove('thesis-slide.tex') }
シェルエスケープ制限にも対応済

周知の通り、TeX\write18 にはセキュリティを考慮して「制限」がかかっている。すなわち既定では特定のコマンドしか実行できず、任意のコマンドの実行を許可するためには --shell-escape オプションを付けてエンジンを起動する必要がある。

LuaTeX の os.execute() はカスタマイズされていて、TeX エンジンのシェルエスケープ制限の設定を遵守する。従って、LuaTeX の os.execute()\write18 の代用として安全に使うことができる。

Lua 文字列の扱いに注意

ここで注意すべきなのは、os.execute('...')'...' の部分は「Lua の文字列リテラル」なので、当然その文法に従う必要がある。つまり〈' " \〉の文字はエスケープする必要がある。((ちなみに、Lua では(PerlRuby と異なり)'...'"..." も同等であり、エスケープが解釈される。エスケープの面倒を軽減するため、Lua では“長括弧”のリテラル[[...]])が用意されているが、これを使ったとしても「何でも気にせずに書ける」わけではない。))

% \immediate\write18{egrep -e '\[\d+\]' __t1 > __t2}
% の代わりにコレ
\directlua{ os.execute('egrep -e \'\\[\\d+\\]\' __t1 > __t2') }
% またはコレ
\directlua{ os.execute("egrep -e '\\[\\d+\\]' __t1 > __t2") }

ところが、コマンド行文字列が可変部分(マクロパラメタ #1 とか)をもつ場合は単純には対処できない。

% マクロの定義本体の中, #1はパラメタ
\immediate\write18{egrep -e '#1' __t1 > __t2}

このような場合のために、LuaTeX では「Lua 文字列リテラルのためのエスケープ」を行うプリミティブ \luaescapestring が用意されている。

\directlua{ os.execute(
  '\luaescapestring{egrep -e '#1' __t1 > __t2}'
) }

ただヤヤコシイことに古い LaTeX*1では \luaescapestring\luatexluaescapestring と書く必要がある(参考)。なので結局、後で紹介する pdftexcmds パッケージを利用する方が手っ取り早いであろう。

pdftexcmds パッケージでシェル実行しよう

pdftexcmds パッケージ(Oberdiek 氏作製)は、「異なるエンジン間でのプリミティブの書き方の差を吸収する」ことを目的としたパッケージである。*2割と新しい TeX 環境であれば最初からインストールされているはずである。

pdftexcmds を読み込むと、\pdf@system という命令((@ を含む名前の制御綴であるが公開命令(マクロ)である。Oberdiek 氏の命名規則では、「開発者(TeX 言語者)向けの公開命令」の制御綴は原則として @ を含む名前になっている。))が提供される。

  • \pdf@system{<コマンド行>} :[一般] シェル実行をサポートするあらゆるエンジンで、(非遅延の)シェル実行を行う。詳細の仕様は \immediate\write18{<コマンド行>} と同一である。
% 新しいLuaTeXでも"その他大勢"でもOK
\pdf@system{rm thesis-slide.tex}

この命令は、LuaTeX エンジン(新しくないものも含む)の場合は os.execute() を利用し、それ以外では \write18 を利用する。((“新しい LuaTeX”以前のパッケージなのに \write18 を避ける対応があるのを不思議に思うかも知れないが、実は大昔の LuaTeX でも \write18 はサポートされていなかったのである。))残念ながら、遅延シェル実行の命令は用意されていないようである。

シェル実行に関連して次のような機能もある。

  • \pdf@shellescape :[展開可能※] シェル実行許可の状態を表す整数値。意味は次の通り(pdfTeX の \pdfshellescape プリミティブと同じ)。
    • 0: 禁止(-no-shell-escape オプションの状態)。
    • 1: 完全許可(-shell-escape オプションの状態)。
    • 2: 制限付(-shell-restricted オプションの状態)。
    • 未定義: エンジンが状態取得の機能を持たないため不明。
    ※展開結果は必ず「整数を表すトークン列」となるが、エンジンの種類によって内部値と数字列のどちらにもなりえることに注意。((\numexpr\pdf@shellescape\relax とすると確実に内部値の扱いになる。(非 e-TeX では \pdf@shellescape 自体が未定義となる。)))
\ifx\pdf@shellescape\@undefined
  % この場合は"不明"であることに注意
\else\ifnum\pdf@shellescape=\@ne
  % 無制限シェル実行が可能
\else
  % 無制限シェル実行は不能
  \@latex@error{OOPS! Shell unavailable}\@ehc
\fi\fi

shellesc パッケージでシェル実行しよう

先日の LaTeX のリリース(2016/02/01 版)に併せて、LaTeXtools バンドルに shellesc というパッケージが追加された。これは \write18 について新しい LuaTeX とそれ以外のエンジンとの互換性を確保するためのものである。*3pdftexcmds の用途(の一部)と重なるが、こちらは遅延シェル実行の命令も提供している。

  • \ShellEscape{<コマンド行>} :[一般] (非遅延の)シェル実行を行う。\immediate\write18{...} と同等。
  • \DelayedShellEscape{<コマンド行>} :[一般] 遅延シェル実行を行う。\write18{...} と同等。
% 新しいLuaTeXでも"その他大勢"でもOK
\ShellEscape{rm thesis-slide.tex}

tools バンドルは LaTeX の必須コンポーネントに含まれるので、LaTeX で必ず利用できることが保証される。とはいっても、つい最近に追加されたばかりのものなので、過去の環境を含めての互換性を確保する目的では使えないであろう。

shellesc パッケージには次のような機能も持っている。

  • パッケージを読み込むと、新しい LuaTeX でもなぜか「\immediate\write18{...}」がこれまで通り使えるようになる。(\immediate でない \write18 は正常に動作しない。((というか \immediate がある場合と同じ動作になる。)))

マニュアルを読んだ感じだと、この機能は開発者(TeX 言語者)でなく文書作成者(幸せな LaTeX ユーザ)のためのもののようである。

% LuaLaTeX 文書
\documentclass[a4paper]{article}
\usepackage{shellesc}
% ↓コイツが動かないのを何とかしたい!
\usepackage{destroy-your-thesis}% \write18を使っているパッケージ!
%......

つまり、開発者が自作のパッケージ(\immediate\write18 している)を新しい LuaTeX に対応させたい場合、パッケージの中で shellesc を読み込めば実はそれで済んでしまうのではあるが、横着せずにきちんと \ShellEscape に書き換えることが望ましい、ということである。

なお、注意であるが、shellesc パッケージは e-TeX 拡張を前提とする。非 e-TeX エンジンでは読み込むだけでエラーが発生してしまう。

参考:自力でやってみよう

「いつでもどこでも*4使えるシェル実行命令」の実装を載せておく。

% \xx@shellescape{<コマンド行>}: (非遅延)シェル実行.
% \xx@delayedshellescape{<コマンド行>}: 遅延シェル実行.
\begingroup\expandafter\expandafter\expandafter\endgroup
\expandafter\ifx\csname luatexversion\endcsname\relax
        % non-LuaTeX
  \DeclareRobustCommand*{\xx@shellescape}{\immediate\write18 }
  \DeclareRobustCommand*{\xx@delayedshellescape}{\relax\write18 }
\else   % LuaTeX
  \directlua{tex.enableprimitives('', tex.extraprimitives())}
  \protected\def\xx@shellescape#1{\directlua{os.execute("\luaescapestring{#1}")}}
  \protected\def\xx@delayedshellescape#1{\latelua{os.execute("\luaescapestring{#1}")}}
\fi

まとめ

  • 幸せな LaTeX ユーザが「新しい LuaTeX で \write18 したらアレ」で困った場合は shellesc パッケージを使おう。
  • TeX 言語者が「新しい LuaTeX で \write18 したらアレ」で困った場合は pdftexcmds パッケージを使おう。

*1:“古い LuaTeX”でなくて“古い LateX”。念のため。

*2:plain TeXLaTeX の両方で使用できる。現在の最新版(0.20 版)のリリース日付は 2011/11/29 であるので、残念ながら LuaTeX 0.81 版以降の LuaTeX のプリミティブの変更自体には対応していない。しかしシェル実行コマンドの問題はこれで解決できる。

*3:plain TeX でも使用可能。

*4:非 e-TeX でも“古い LaTeX”でも plain TeX でも。