どうも!25B、Algorithm/Graphic/Game/SysAd/Kaggle/Sound(多くないですか)の@Suima です。今日はTeX言語のお話をちょびっとだけしていきます。
そもそも、TeX言語って何だよ
いや、ごもっともな話です。殆どの人間がTeXといった場合、日本ではLaTeX(それも、(u)pLaTeXかLuaLaTeX)のことを指しています。
多少入り込んでしまった人でもplain-TeXのことを大抵は意図しています。私もそれでいいと思います。
(あと、ConTeXtの話を始めてしまいそうになった(日本ではマイナーという意味で)やばい貴方は、ConTeXtを私に教えてください。)
ただ、今回はTeX言語の部分をお話ししていこうと思います。
凄く簡単に言えば、組版処理を指示することに特化したプログラミング言語がTeX言語です。
TeX言語をそのまま運用すると非常に面倒(組版処理の非常に低レイヤでの機能のみを有しているため、逆に普段の組版作業では指示しなければならない内容が非常に多くなる)なので、plain-TeXやLaTeX(あとConTeXt(歴史的に細かく言えばamsTeXとかも))などのマクロパッケージが開発されていったというわけです。
これらのマクロパッケージは、TeXのうち、通常の組版をする上ではユーザーが意識しなくて良いような操作をある程度(あるいは、ほぼ完全に)自動で行ってくれたり、TeX言語を書きやすいように改善したり、マクロの処理の安全性を改善したりしてくれています。
しかし、TeXをプログラミング言語として扱いたい酔狂な人間がいたとしても、学ぶことは非常に困難です。
というのも、TeXといった場合LaTeXの情報しか手に入らない、漸く辿り着いても情報源はほんのわずか、本で調べようと思うとTeXブック(ASCII)は絶版。(まあ英語版はTeX Liveに収録されているんですが、なかなかしゃれた言い回しをしているため…ぶっちゃけ、読みづらいです)
といった状況だからです。
ので、このブログでは
1. TeX言語の使い方
2. 標準入出力
3. 簡単なプログラミング
について扱っていきます。やったね
TeX言語の使い方
TeX言語の使い方は幾つかありますが、今回は最初にplain-TeXを利用する方法を紹介していきます。
plain-TeXはTeX Liveをインストールしていれば必ず使えます。
TeX Liveのインストールは各自調べてください(WindowsユーザーならWSL上に入れることをお勧めします)
- main.texをつくる
- main.tex があるディレクトリでシェルを開く
- tex main と打つ
すると、*が表示されます
この状態で、 Hello \TeX\ !\bye
と打ち込んでみてください
Output written on main.dvi ほげほげ~
と出力されますから、続けてdvipdfmx main と打ち込むと、main.pdfが出力されるので、見てください。

どうみてもHello TeX !ですね。最高。
まあぶっちゃけ、この辺はまだ簡単ですね(強いて言えば \bye
は目新しいかもしれませんが、これは単に処理終了を意味するコントロールシーケンス(制御綴)です。
何が起きたのか-解説-
簡単に説明します。
- main.tex上でtexを起動する(texは、拡張子が抜けていればそこに.texを補うため、tex mainではmain.texを読み込みます)
- (ここはtexの対話型実行環境)
Hello \TeX\ !\bye
をmainの内容(空ですから、何もありません)に加えて実行する - \byeがあるため、この組版結果をmain.dviに保存し、その処理経過をmain.logに出力する
- dvipdfmxを起動し、main.dviを読み込ませ、(dvipdfmxは、拡張子が抜けていれば.dviを補う)main.pdfが出力される。
という流れになっています。
実は対話型実行環境をサポートしているのがTeXなんですね~ つよそう。
TeX言語の基礎
ここでは、TeX言語の基礎的な事項を確認しておきます
- ファイルの開始は唐突
- ファイルの終了は
\end
この二つが重要事項です。
基本的に、マクロの定義領域以外に書いたことは制御綴含み全て実行されると考えてもらっても差し支えないです。(このへんはシェルスクリプトのバッチファイルと似た雰囲気がありますね)
(因みに、先ほど使用した\bye
はplain-TeXで定義されたマクロで、す。また、LaTeXでは\end
は環境終了のコマンドとして再定義されています)
標準入出力
さて、重要事項として、TeXには標準入出力が存在します。
と言いたいところですが、ないんですよね…いやあるんですが…
はっきりしてくれって?
TeXは、(条件付きで)シェルを実行できます。
条件付きというのは、基本的には安全のためにその機能はロックされており、 --shell-escape
オプションを付けると使えるようになります。
言い換えると、これからの話はある程度シェルスクリプトの知識がないのなら、むやみに試すのはお勧めしません。
write命令について
ここからがこのブログの本題です。
TeXにはwriteというプリミティブ命令があります(つまり、TeX言語自体の命令としてもっています)
\writeストリーム番号{記述内容}
の形式で記述することができ、ストリーム番号に対応する対象に対して、記述内容を書き込むという動作になります。。
ここで、0-15番のストリーム番号はユーザーが開いたファイルに割り振られますが、
16番はターミナルとログに対しての出力
17番はログと(通常では)ターミナルに対しての出力
そして、18番はシェルコマンドとしての実行に割り振られています
そして、この場合にはシェルコマンドを実行しているプロセスは勿論そのtexファイルですから、
- このシェルコマンドで標準入力から一時ファイルに書き込む
- その一時ファイルをtexで開く
- 開いた一時ファイルから一行ずつ読み出す(writeの双対ともいえるreadで出来ます)
という非常に面倒な作業によって、texの標準入力から入力を受け取って処理に回すことが出来ます。何の利点が? (真面目な話をすると、処理結果を自動的に組版する、など色々ありますが)
具体的にやってみましょう。
\immediate\write18{read -r line <&0 && echo "$line" > stdin.tmp}
\newread\infile
\openin\infile=stdin.tmp
\ifeof\infile
\def\fromstdin{FAILED to read from stdin.}
\else
\read\infile to \fromstdin
\fi
\closein\infile
Input from stream 0: {\tt\fromstdin}
\bye
stdin in TeX !
hogefuga
cat data.txt | tex --shell-escape standardio && dvipdfmx standardio
こうすると、standardio.pdfが出力されて、中身にstdin in TeX !が含まれているはずです。
かなりコードの見た目が「古いプログラミング言語」感が強いですね(実際「古いプログラミング言語」な訳ですが)
非常に、手間がかかりましたね
INITEXとTeXプログラミング
ここからは、TeXプログラミングをするので、「純粋なTeX言語処理系」を呼び出す方法からやっていきます
TeXは通常、組版を行うための体系(マクロパッケージ)を伴っています。(texやptexと打ち込んで尚plain.texが読み込まれます)
しかし、純粋なTeX処理系を呼び出すことも出来、-iniオプションを使うことで呼び出すことが出来ます。
更に、iniだけでなく、-interaction=batchmodeオプションを掛けるとコンソールログも減ります(\write17はbatchmodeだとlogにしか出力しない、というのが先ほどの煮え切らない記述の原因です)
ですが、INITEXは\newif
が使えなくて(plain-TeXやLaTeXで定義されているマクロ命令のため)、プログラミングをするにはやっかいという次元ではありません。ぶっちゃけ死亡に近い。
ので、今から移植します。
(このコードはTeX Liveに収録されている、plain-TeXのマクロパッケージとしての実体ファイルであるplain.texから直接(関係ある部分を)引用したものです。但し、コメントアウトの内容は私によって改編されています)(因みに結果から言えばこのコードは今回のブログでは使用しません)
\catcode`\@=11% @は文字である(大本営発表)
\catcode`\{=1% {はグルーピングの開始文字
\catcode`\}=2% }はグルーピングの終了文字
\countdef\count@=255% 変数宣言
\countdef\m@ne=22%
\m@ne=-1% 定数宣言
\outer\def\newif#1{\count@\escapechar \escapechar\m@ne
\expandafter\expandafter\expandafter
\def\@if#1{true}{\let#1=\iftrue}%
\expandafter\expandafter\expandafter
\def\@if#1{false}{\let#1=\iffalse}%
\@if#1{false}\escapechar\count@}
\def\@if#1#2{\csname\expandafter\if@\string#1#2\endcsname}
{\uccode`1=`i \uccode`2=`f \uppercase{\gdef\if@12{}}}
このコードの本質的な部分を説明していきましょう(ぶっちゃけHello Worldよりよほど難解です) (因みにINI TEXではcatcodeが殆ど定義されていない(グルーピングの開始/終了文字が定義されていない)ので、ちゃんとおまじないとして宣言してあげる必要があります
また、 \countdef\hoge=<int>
というのは、 <int>
番のカウンタ名を \hoge
にする、という意味です。(特に、255番カウンタは慣習として汎用の変数として利用されます)
\outer
:「続いて定義されるマクロはマクロの定義内部では呼び出せませんよ」\def\newif#1
:「形式に条件のない変数を一つ持つマクロ\newif
を定義します\count@\escapechar \escapechar\m@ne
:「エスケープ文字に割り振られている文字コードを\count@
に代入して、 エスケープ文字に割り振る文字コードを-1
にします」(TeXはカウンタ同士の場合は=が省略できます)\expandafter\expandafter\expandafter \def\@if#1{true}{\let#1=\iftrue}
: ここ、やばいので次の段落で説明します。\expandafter\expandafter\expandafter\def\@if#1{false}{\let#1=\iffalse}
: 4. と同様の処理のfalse版\@if#1{false}
:\if@
に第一変数とfalseを代入\def\@if#1#2
:「形式に条件のない変数を二つ持つマクロ\@if
を定義します\csname~\endcsname
: 「囲んだ部分の文字列を名前に持つマクロを定義します」\expandafter\if@\string#1#2
: ここ、やばいので次の段落で(ry- `{\uccode`1=`i \uccode`2=`f ~}` :このグルーピング内では1の大文字をi,2の大文字をfとします
\uppercase{\gdef\if@12{}}
: このグルーピング外でも有効で、変数を二つ持っていて、その変数は1と2であるマクロ\if@
を定義しますが、1と2は大文字に置き換えてください(つまり、ifという文字列を認識して、それを変数とするマクロを定義します)
何事ですか?????
4, 5, 9 のやばさはTeXの \def
がもつ「パターンマッチ」と \expandafter
からきます。
\expandafter
は、直後に来たトークン(1制御綴または1文字)の「構造の評価」を一段階遅延します。
このため、 \expandafter\expandafter\expandafter \def\@if#1{true}{\let#1=\iftrue}
の評価は、先ず \@if#1{true}
から評価されます。(1番目の \expandafter
によって2番目の \expandafter
が遅延され、3番目の \expandafter
によって \def
の評価が遅延するため)
じゃあこの \@if
は一体…?
直後にありますね。 \@if
は変数を二つ受け取って、それらを元に制御綴を生成するマクロです。ここでの定義に \if@
が使われていますね。 \if@
の定義は空なので、11での話も踏まえると意味的には「後に文字i,fが来たらそれを空文字に置換する(さもなくば変数のパターンが一致しないのでエラー)」というものになりますね。
話を戻すと、 \@if
は#1(ここでは \iffuga
とします)を単なる文字列として認識し(これは \ifhoge
の形をしています) 文字列 \fuga
と#2を結合し、それを名前に持つ制御綴を生成する、というものです
ここで役者がそろいました。 \expandafter\expandafter\expandafter \def\@if#1{true}{\let#1=\iftrue}
という怪文書は、「 \iffuga
の形の文字列を受け取った場合に「制御綴 \fugatrue
を、「 \iffuga
を \iftrue
と同じ意味のマクロとしてあつかう」という意味のマクロとする」」、という意味でした(これらは全て一つの \newif
というマクロの定義の内部なので、この怪文書が処理されるのは特性上 \if@
の後が望ましいので、このような構造になっています。
さて、ここからはプログラミングのお時間です。
Hello World!
つまり、initexとしてHello Worldをします。
\catcode`\{=1%
\catcode`\}=2%
\write18{echo Hello World!}
\end
EVEN ODD
入力は以下の形式で渡される
N
a_1
a_2
︙
a_N
a_nについて、偶奇を判定し、偶数ならEVEN、奇数ならODDと、改行区切りで出力せよ。
答え
\catcode`\@=11
\catcode`\{=1
\catcode`\}=2
\catcode`\#=6
\count255 = 255
\def\newcount#1{\advance\count255by-1\countdef#1=\count255}%
\immediate\write18{read -r line <&0 && echo "$line" > amount.tmp && echo}%
\def\AM{}
\newcount\amount
\newcount\numbtmp
\openin0=amount.tmp
\read0 to\AM
\closein0
\amount = \AM
\def\evenodd#1{%
\numbtmp = #1
\ifodd\numbtmp
\immediate\write18{echo ODD}
\else
\immediate\write18{echo EVEN}
\fi
}
\def\loopread{%
\advance\amount by-1
\immediate\write18{read -r line <&0 && echo "$line" > evenodd.tmp}
\openin0 =evenodd.tmp
\global\read0 to\numb
\closein0
\evenodd\numb
\ifnum\amount<1
\else
\loopread
\fi
}
\loopread
\end
FIZZBUZZ
はい、皆さん大好きFIZZBUZZです
答え
\catcode`\{=1%
\catcode`\}=2%
\catcode`\#=6%
\count255 = 255%
\def\newcount#1{\advance\count255by-1\countdef#1=\count255}%
\immediate\write18{read -r line <&0 && echo "$line" > line.tmp && echo}%
\relax%
\newcount\maxnum
\newcount\inter%
\newcount\modval%
\newcount\tempval%
\newcount\remthree%
\newcount\remfive%
\newcount\remfifteen%
\def\temp{}%
\openin0=line.tmp%
\read0 to\temp%
\maxnum=\temp%
\closein0%
\inter=0%
\def\domod#1#2#3{%
\ifnum#2=0
\else%
\ifnum#1=0
\else%
\modval=#1%
#3=#1%
\divide\modval by#2%
\multiply\modval by#2%
\global\advance#3 by-\modval%
\fi\fi%
}%
\def\fizzbuzz{%
\advance\inter by1%
\ifnum\inter=0
\else%
\domod\inter3\remthree%
\domod\inter5\remfive%
\domod\inter{15}\remfifteen%
\ifnum\remfifteen=0
\immediate\write18{echo FIZZ BUZZ}%
\else%
\ifnum\remthree=0
\immediate\write18{echo FIZZ}%
\else%
\ifnum\remfive=0
\immediate\write18{echo BUZZ}%
\else%
\immediate\write18{echo \the\inter}%
\fi%
\fi%
\fi%
\fi%
\ifnum\inter<\maxnum%
\fizzbuzz%
\fi%
}%
\immediate\write18{echo }%
\immediate\write18{echo 1}%
\ifnum\maxnum=1
\else%
\fizzbuzz%
\fi%
\end
もうちょい賢い実装もあるかもしれませんが、私にはよく分かりませんでした。
では、またいつか~~