TeXにとってやばい入力ファイル名

空白や記号類を名前に含むファイル名をTeXで処理させようとすると、うまく処理できないことがある。

例えば foo%bar.tex というファイルを latex foo%bar.tex のような感じでLaTeXに処理させようとすると、 % 以下の部分がコメント扱いされ、LaTeXは代わりに foo(.tex) というファイルを処理しようとする。

やばい名前のファイルを難しいことを考えずに処理できればいい方へ

8月中にリリースされる ClutTeX v0.4 を使って cluttex -e latex foo%bar.tex とすれば良い。

どういうファイル名がやばいのか、ClutTeX v0.4はどのように問題を解決するのか知りたい方へ

例によって結論に至るまでの経過をくどくど書いているので、結論だけ知りたい方は適当に読み飛ばすことをお勧めする。

目次

関連するツイート等を適当に貼っておく。

以下、シェルの特殊文字は適宜クォート等で無効化する前提とする。コマンドの実行例はUnixのシェルを前提(シングルクォートでほとんどの文字が無害になる)とする。

TeXのコマンドライン引数はどのように解釈されるか

先頭の文字がバックスラッシュ(より正確にはカテゴリーコード0の文字)であればそれがTeXコードとして実行される。例えば、

pdftex '\message{Hello world!}\bye

を実行すると \message{Hello world!}\bye がTeXコードとして実行される。

そうでない場合は、コマンドライン引数がファイル名として解釈される……のではなく、先頭に \input が付加されたかのように動作する。つまり

pdflatex 'foo%bar.tex'

を実行すると、\input foo%bar.tex 相当のTeXコードが実行されて、(%bar.tex はコメントとして解釈されて) foo というファイルを読み込もうとする。

(TeX上級者向け:「先頭に \input が付加されたかのように」の部分は2つの意味で正しくない。まず、(ZR氏の記事「TeX コマンドの引数を理解したい話(補足)」にもあるように )付加され、実行されるものはその時点で \input という名前がついているコマンドではなく、TeX primitiveの \input 、言わば ‘frozen’ \input である。第2に、この ‘frozen’ \input の引数が展開されるのは、 \everyjob の内容が実行される前である。つまり、 pdflatex '^^7f'\everyjob の内容が実行される前にエラーになるのでLaTeXのバージョン情報が表示されないが、 pdflatex '\input ^^7f'\everyjob の内容が実行され、LaTeXのバージョン情報が表示される。\everyjob 実行の有無は、後述する「pdfLaTeXでの非ASCII文字の取り扱い」に関わってくる。)

(先頭が & なやつはこの記事では扱わない)

どういう特殊文字が問題になるか

まず、どういう文字がどういう状況で問題になるかを考える。

1. コマンドラインでファイル名として渡す際に問題になる文字

  • %:コメント文字として解釈される。
  • ^:単独で使う場合は問題ないが、2個以上並べて制御文字を入力する記法(^^J 等)として解釈されてしまうとまずい。
  • &:先頭に置いた場合はフォーマット名として解釈される?ので問題になる。先頭じゃない場合や、自分で書いた \inputの後であれば問題ない。
  • ~:アクティブ文字なので。
  • バックスラッシュ、ダブルクォート、NULとか改行とか:ファイル名に使う奴はおらんやろw

2. コマンドライン引数に直接渡す場合は問題ないが、\input の後に使うとやばい文字

  • 空白:\input後のファイル名の終わりを示すために使われる。
  • (pdfLaTeXの場合は「非ASCII文字」もここの分類に入る)

つまり、 latex 'foo bar.tex' という風に実行する際は問題にならないが、 latex '\input foo bar.tex' とすると問題になる。

3. 一旦 \edef する場合に問題になる文字

  • #, }, {\input にそのまま与える場合は問題ない(\input foo}{bar.texfoo}{bar.tex というファイルを読み込める)が、ファイル名を \edef する際に問題となる。:

おまけ:特殊文字だけど問題ないやつ

  • _, $

$ はkpse関連のやつで解釈される?よくわからんけど対策のしようがなさそうなのでここでは扱わない)

特殊文字・空白文字への対策(ASCIIの範囲内)

ステップ1. \string

% とか ~ のカテゴリーコードがやばいとやばいので、カテゴリーコードがやばくない %~ を生成する必要がある。

\%\~ 等の1文字制御綴に対して \string を適用するとカテゴリーコード12なトークンの列になる。ただ、通常の状態で制御綴に対して \string を適用すると先頭にバックスラッシュが付加される。

\string がバックスラッシュを付加しないようにするには、 \escapechar レジスターに負の数を代入しておけば良い。(ちなみに、LuaTeXには制御綴の文字列化の際にエスケープ文字を付加しない \csstring というプリミティブがある)

というわけで、 foo%bar.tex という名前のファイルをTeXで処理するには

\escapechar-1 \input foo\string\%bar.tex

とすれば良い。ただ、このままでは色々問題がある。

ステップ2. 省略表記

まず、1文字をエスケープするのにいちいち \string\% と書くのはだるい。長さが9倍になっている。これに対しては、 ~ のような既存のアクティブ文字に \string を代入しておいて ~\% とすれば良い。

\escapechar-1\let~\string\input foo~\%bar.tex

ステップ3. スコープの制限

ただ、これでは \escapechar~ の意味を変えた状態で文書の処理を始めてしまうことになる。それではまずいので、\begingroup\endgroup でスコープを作り、その中で \edef によりファイル名の文字列を表すマクロ(名前は適当に \x とする)を定義する。その後、スコープを抜けた段階で \input\x をすれば良い。

\begingroup\escapechar-1\let~\string\xdef\x{foo~\%bar.tex}\endgroup\input\x

ステップ4. \expandafter

ただ、これでは結局 \x の意味を変えた状態で処理を始めてしまっている。これをどうにかするには、\x をグローバルに定義するのをやめ(\xdef ではなく \edef を使う)、\endgroup が実行される前に \expandafter\input\x\x を先に展開してしまう。

\begingroup\escapechar-1\let~\string\edef\x{foo~\%bar.tex}\expandafter\endgroup\expandafter\input\x

ステップ5. \input の終端

こうすると大体良さそうに思えるが、実際に実行してみると処理が行われず、入力を求められる。-interaction=nonstopmode を指定している場合は

! Emergency stop.
<*> ...x}\expandafter\endgroup\expandafter\input\x

というエラーが出る。

これはなぜかというと、\input に与えるファイル名が終了していないからである。普通に \input foo.tex と書いた場合は、行末の ^^M\endlinecharに依る)が空白に変換されるので \input へのファイル名が閉じられる。だが、行の最後の文字が \x だと、行末の空白が制御綴に吸収されてしまい、 \input に与えるファイル名が終了しない。

この対策としては、 \x の定義の際にあらかじめ末尾に空白を置いておけば良い。

\begingroup\escapechar-1\let~\string\edef\x{foo~\%bar.tex }\expandafter\endgroup\expandafter\input\x

.tex の後に空白を置いた。

\expanded がある最近のTeX処理系であれば \begingroup\escapechar-1\let~\string\expandafter\endgroup\expandafter\input\expanded{foo~\%bar.tex} と書いても良い。この場合は行末の空白は吸収されないので、余計な空白を置いておく必要はない。)

ステップ6. ファイル名の中の半角スペース

大概の特殊文字は \string\〈文字〉 という形で効力をなくせる(カテゴリーコードを12にできる)が、半角スペースに関しては \string\ としてもカテゴリーコードが10(空白)のままである。というかそもそも、 \input に関しては文字のカテゴリーコードは関係なく、文字コードが32だったら半角スペース扱いされるようである(\lowercase トリックも無意味)。

じゃあどうするかというと、解決策は「ダブルクォートで囲う」となる。詳しくは以下のZR氏の記事を参照。

というわけで、

\begingroup\escapechar-1\let~\string\edef\x{"〈エスケープした入力ファイル名〉" }\expandafter\endgroup\expandafter\input\x

とすれば大抵のファイル名は大丈夫なはずである。

なお、ダブルクォートで囲えばファイル名中の半角スペースをエスケープしなくて良いかというとそうでもなくて、2個以上の半角スペースが並んでいた場合に何もしないと字句解析時に後ろの半角スペースが吸収されてしまう。なので、半角スペースが2個以上並んでいる場合は適宜エスケープしておく必要がある。

jobnameの扱い

記号の入ったファイル名が問題になるのは、TeX処理系の起動時だけではない。入力ファイル名は \jobname のデフォルト値にも影響するので、 \jobname にも影響が及ぶ。\jobname は例えば補助ファイル(.aux とか)の名前に使われるので、補助ファイルの名前に記号類が入る可能性がある。まあ、普通にファイルを読み書きする分には問題にならないのだが……

外部コマンドに読み書きさせる補助ファイルの名前にシェルの特殊文字が入っていると、問題が起こる。

この問題を、実際のパッケージで確かめてみよう。ここでは gnuplottex を使ってみる。(ちなみに現在のClutTeXではこのgnuplottexをうまく扱えない)

\documentclass{article}
\usepackage{shellesc} % ←LuaTeXでは必要
\usepackage{gnuplottex}
\begin{document}
Hello
\begin{gnuplot}
plot sin(x)
\end{gnuplot}
\end{document}

このファイルを hello world (1).tex という名前で保存して、 pdflatex -shell-escape "hello world (1).tex" を実行する。すると、

sh: -c: line 0: syntax error near unexpected token `('
sh: -c: line 0: `rm -f ""hello world (1)".gnuploterrors"'
system returned with code 512

というエラーが出て、 sin(x) のグラフが挿入されない。

この問題を回避するには、処理時に pdflatex -shell-escape -jobname=hello_world_1 "hello world (1).tex" という風に「安全な」 jobname を指定すれば良い。

エスケープする対象の文字は、使用されるシェルで危なそうな文字である。空白もエスケープしておくと良いだろう。「危なそうな文字」というのはUnixのシェルとWindowsのコマンドプロンプトでも変わってくると思う。

ちなみに、mintedのような気合の入ったパッケージは、 \jobname に記号が入っていても大丈夫なように使用する際に独自のエスケープ処理を行っているようだ。

なお、jobnameをエスケープすると、 \jobname.tex で処理対象のTeX文書ファイルが参照できることを期待しているコードが壊れる可能性がある。

筆者が思いついた \jobname.tex の使用例:

非ASCIIファイル名

ファイル名に入るとやばいのは記号類だけではない。日本語ファイル名、というか非ASCIIファイル名も問題を起こしうるというのはみなさんよくご存知だろう。

※以下、WindowsみたいなレガシーOSのことは忘れます。TeXエンジンに対するコマンドライン引数はUTF-8エンコードされ、fopenに渡すファイル名もUTF-8でOKな環境を想定します。

Unicodeなエンジンの場合

LuaTeX, XeTeXはUnicodeファイル名を問題なく扱えるはず。upTeXも大丈夫そう。

pdfTeXの場合

pdfTeXは8ビットクリーンで、エンジン自体はUTF-8のことを知らない。「TeX Live 2018からLaTeXでUTF-8がデフォルトになった」というのは「LaTeXが頑張ってTeX言語の力でUTF-8を解釈するようになった」という話である。

つまり、LaTeXは8ビット文字のうち8ビット目が立っているもの(文字コード0x80から0xffまで)をアクティブ化してTeXコードでUTF-8を解釈しているのである。マジやばくね?(従来はinputencパッケージを読み込むことでこれが行われていたが、TeX Live 2018以降は何も読み込まなくてもこれが行われるようになった)

LaTeXがTeX言語で頑張った結果、UTF-8でエンコードされた非ASCII文字がどうなるかというと、

  • 当該文字が \DeclareUnicodeCharacter によって定義されていた場合は、そのトークン列
  • そうでなければ、エラー(を生成するトークン列)

になる。前者は多くの場合「LaTeXでその文字を印字するための命令列(例: â に対して \^a)」として定義されており、大抵は展開できない制御綴で始まるトークン列となる。後者も展開不能な制御綴から始まるトークン列である。というわけで、例えば

\input café.tex

と書いた場合に \input の後ろに来るのは

caf〈展開できない制御綴で始まるトークン列〉.tex

となり、 \inputcaf の部分をファイル名として解釈しようとする。

(一方、同じファイルを pdflatex café.tex と処理した場合は問題なく café.tex が読み込まれる。pdflatex café.texpdflatex '\input café.tex' と同等だと思うと一見不可解だが、この記事の最初の方の上級者向け注意で書いたように、 \everyjob の中身の実行タイミングの関係でこの違いが生まれる。)

対策としては、例によってファイル名の非ASCIIな部分をエスケープして caf~\^^c3~\^^a9.tex という風にしてやれば良いだろう。あるいは\detokenize を使う方がポピュラーかもしれない。

(なお、WindowsのようなレガシーOSでpdfTeXには command_line_encoding=utf8 が適用されてLuaTeXで動くClutTeXにそれが適用されない場合………やめておこうこの話は)

pTeX

よくわからん。まあ内部JISなpTeXはもはやレガシーなのでさっさとupTeXかLuaTeXに移行しましょう。どうしても移行できないような過去の資産はファイル名に非ASCII文字(非JIS文字、という方が正確なのか)なんて使ってないだろうから問題ないでしょ。

やばいファイル名を処理できるようにすることは正義なのか

TeXの特殊文字を含むファイル名というのはTeXの文化に即さない。ClutTeXのようなツールで頑張ってコマンドライン引数でやばいファイル名を与えられるようになったとして、ファイル中で \input foo%bar.tex とされたら対策のしようがない。一貫性を重視して「やばいファイル名をエスケープして頑張るのではなく、最初からエラーとして弾いてしまう」とした方が良い、という考え方もあるだろう。

例えば、latexmkに foo%bar.tex のようなやばいファイル名を与えると、次のようなエラーとなる:

Latexmk: Filename 'foo%bar.tex' contains character not allowed for TeX file.
Latexmk: Stopping because of bad filename(s).

ただ、TeX文書中の \input foo%bar.tex は明らかにTeXコードなのでほとんどの人は % がコメントである(のでうまくいかない)ことに気づくはずなのに対し、 pdflatex foo%bar.tex と書いた時の foo%bar.tex がファイル名じゃなくてTeXコードとして解釈されるということはそんなに自明ではない。普通のツールはコマンドライン引数にファイル名っぽいものを与えた時は、それはコメントを含みうるコード片ではなく、字句そのままファイル名として解釈するものだ。

私の考えとしては、ClutTeXはなるべくTeX独自の流儀を廃して、他のコマンドラインツールと同様に動作してほしい。コマンドラインで動かす普通のコンパイラーはエラー時にユーザーに入力を求めないし、普通はコマンドラインで与えるファイル名っぽい引数はファイル名であって途中のコメント文字が解釈される代物ではない(例:lua foo--bar.lua を実行した時には foo ではなく foo--bar.lua が実行される)。ClutTeXもそうであってほしい。