feature image

2025年8月2日 | ブログ記事

TeX言語入門入門

どうも!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上に入れることをお勧めします)

  1. main.texをつくる
  2. main.tex があるディレクトリでシェルを開く
  3. tex main と打つ

すると、*が表示されます
この状態で、 Hello \TeX\ !\byeと打ち込んでみてください
Output written on main.dvi ほげほげ~
と出力されますから、続けてdvipdfmx main と打ち込むと、main.pdfが出力されるので、見てください。

どうみてもHello TeX !ですね。最高。

まあぶっちゃけ、この辺はまだ簡単ですね(強いて言えば \byeは目新しいかもしれませんが、これは単に処理終了を意味するコントロールシーケンス(制御綴)です。

何が起きたのか-解説-

簡単に説明します。

  1. main.tex上でtexを起動する(texは、拡張子が抜けていればそこに.texを補うため、tex mainではmain.texを読み込みます)
  2. (ここはtexの対話型実行環境) Hello \TeX\ !\byeをmainの内容(空ですから、何もありません)に加えて実行する
  3. \byeがあるため、この組版結果をmain.dviに保存し、その処理経過をmain.logに出力する
  4. dvipdfmxを起動し、main.dviを読み込ませ、(dvipdfmxは、拡張子が抜けていれば.dviを補う)main.pdfが出力される。

という流れになっています。

実は対話型実行環境をサポートしているのがTeXなんですね~ つよそう。

TeX言語の基礎

ここでは、TeX言語の基礎的な事項を確認しておきます

  1. ファイルの開始は唐突
  2. ファイルの終了は \end

この二つが重要事項です。

基本的に、マクロの定義領域以外に書いたことは制御綴含み全て実行されると考えてもらっても差し支えないです。(このへんはシェルスクリプトのバッチファイルと似た雰囲気がありますね)
(因みに、先ほど使用した\byeはplain-TeXで定義されたマクロで、す。また、LaTeXでは\endは環境終了のコマンドとして再定義されています)

標準入出力

さて、重要事項として、TeXには標準入出力が存在します。

と言いたいところですが、ないんですよね…いやあるんですが…

はっきりしてくれって?

TeXは、(条件付きで)シェルを実行できます。
条件付きというのは、基本的には安全のためにその機能はロックされており、 --shell-escapeオプションを付けると使えるようになります。

言い換えると、これからの話はある程度シェルスクリプトの知識がないのなら、むやみに試すのはお勧めしません。

write命令について

ここからがこのブログの本題です。
TeXにはwriteというプリミティブ命令があります(つまり、TeX言語自体の命令としてもっています)

\writeストリーム番号{記述内容} の形式で記述することができ、ストリーム番号に対応する対象に対して、記述内容を書き込むという動作になります。。
ここで、0-15番のストリーム番号はユーザーが開いたファイルに割り振られますが、
16番はターミナルとログに対しての出力
17番はログと(通常では)ターミナルに対しての出力

そして、18番はシェルコマンドとしての実行に割り振られています

そして、この場合にはシェルコマンドを実行しているプロセスは勿論そのtexファイルですから、

  1. このシェルコマンドで標準入力から一時ファイルに書き込む
  2. その一時ファイルをtexで開く
  3. 開いた一時ファイルから一行ずつ読み出す(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
standardio.tex
stdin in TeX !
hogefuga
data.txt
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番カウンタは慣習として汎用の変数として利用されます)

  1. \outer :「続いて定義されるマクロはマクロの定義内部では呼び出せませんよ」
  2. \def\newif#1 :「形式に条件のない変数を一つ持つマクロ \newif を定義します
  3. \count@\escapechar \escapechar\m@ne :「エスケープ文字に割り振られている文字コードを \count@ に代入して、 エスケープ文字に割り振る文字コードを -1 にします」(TeXはカウンタ同士の場合は=が省略できます)
  4. \expandafter\expandafter\expandafter \def\@if#1{true}{\let#1=\iftrue} : ここ、やばいので次の段落で説明します。
  5. \expandafter\expandafter\expandafter\def\@if#1{false}{\let#1=\iffalse} : 4. と同様の処理のfalse版
  6. \@if#1{false} : \if@ に第一変数とfalseを代入
  7. \def\@if#1#2 :「形式に条件のない変数を二つ持つマクロ \@if を定義します
  8. \csname~\endcsname : 「囲んだ部分の文字列を名前に持つマクロを定義します」
  9. \expandafter\if@\string#1#2 : ここ、やばいので次の段落で(ry
  10. `{\uccode`1=`i \uccode`2=`f ~}` :このグルーピング内では1の大文字をi,2の大文字をfとします
  11. \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
これ、殆どbashじゃない?

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
因みに最初にバナー出力が出るため、競プロでは常に負ける言語である。(出力エラー)

もうちょい賢い実装もあるかもしれませんが、私にはよく分かりませんでした。

では、またいつか~~

Suima icon
この記事を書いた人
Suima

LaTeXぺちぺち

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】 feature image
2018年11月3日
ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】
Azon icon Azon
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2023年4月27日
Vulkanのデバイスドライバを自作してみた
kegra icon kegra
2024年4月14日
Spotifyのクライアントを自作しよう
d_etteiu8383 icon d_etteiu8383
2022年3月29日
課題・レポートの作成、何使う?【新歓ブログリレー2022 21日目】
aya_se icon aya_se
2021年12月8日
C++ with JUCEでステレオパンを作ってみた【AdC2021 26日目】
liquid1224 icon liquid1224
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記