LaTeX の環境には、実のところ特別な仕組みは何もありません。\begin{quote} は \quote というコマンドを呼び、\end{quote} は \endquote というコマンドを呼ぶ——それだけです。環境を自作する \newenvironment がしているのも、この 2 つのマクロをまとめて書くことにすぎません。この対の構造さえ掴んでしまえば、あとは芋づる式です。なぜ環境名がコマンド名と衝突するのか、なぜ引数が終了コードでは使えないのか、\begin と \end が食い違ったときのエラー文がなぜあの形なのか。このページでは \newenvironment と \renewenvironment、引数とオプション引数、ただで付いてくるグループ化、そして現代的な \NewDocumentEnvironment までを、その一本の筋で追いかけます。
\newenvironment の書き方——開始コードと終了コード
プリアンブルに \newenvironment{name}{開始コード}{終了コード} と書けば、本文で \begin{name}…\end{name} が使えるようになります。第 1 引数は環境の名前で、バックスラッシュは付けません。第 2 引数が \begin{name} に出会ったときに実行されるコード、第 3 引数が \end{name} に出会ったときに実行されるコードです。挟まれた本文そのものには手が加わらず、ふつうに組まれます。つまり自作環境の設計とは、「入口で何を仕込み、出口で何を片づけるか」を決めることに尽きます。
% preamble: define a warning environment
\newenvironment{warning}{%
\par\noindent\textbf{Warning:}\itshape
}{%
\par
}
% body: use it
\begin{warning}
This operation cannot be undone.
\end{warning}この例で終了コードが \par ひとつだけなのは、手抜きではありません。開始コードで掛けた \itshape(斜体)を戻す処理がどこにも無いのに、環境の外はきちんと立体に戻ります。理由は次の 2 節で明かしますが、先に結論だけ言っておくと、環境は自動的に「グループ」になるからです。\newenvironment を書くときは、戻さなくてよいものを戻そうとしない のがコツで、終了コードは空 {} でも構いません。
環境の正体は \name と \endname の 2 つのマクロ
\begin{name} は \name を、\end{name} は \endname を呼び出します。これは推測ではなく、latex.ltx に書いてある \end の定義がそのまま \csname end#1\endcsname だからです。信じられなければ、TeX の \show で覗いてみるのが早道です。標準の quote 環境について \show\quote と \show\endquote を実行すると、片方は \list を開くマクロ、もう片方はただの \endlist だと分かります。環境という「構文」はどこにもなく、あるのは名前で対にされた 2 つのマクロだけです。
% ask LaTeX what the quote environment is actually made of
\show\quote
% > \quote=\long macro:
% -> \list {}{\rightmargin \leftmargin }\item \relax .
\show\endquote
% > \endquote=\long macro:
% -> \endlist .
% so \begin{quote} ... \end{quote} is, in effect:
% \begingroup \quote ... \endquote \endgroupこの事実には、すぐに実害の形で出会います。環境名は、同じ綴りのコマンド名を占有します。 ためしに \newenvironment{alpha}{...}{...} と書くと、コンパイルは ! LaTeX Error: Command \alpha already defined. で止まります。ギリシャ文字の \alpha が既にあるからで、環境の名前空間とコマンドの名前空間は同じ 1 つだった、というわけです。\newenvironment{quote} が Command \quote already defined. になるのも同じ理由で、これは事故防止のための意図的な検査です。自作環境の名前は mywarning、thmbox のように、既存コマンドと衝突しにくい語を選ぶのが安全です。
同じ理屈の裏返しが、\newcommand の側にある有名な制約です。\newcommand{\endnotes}{...} は、そんなコマンドがどこにも無いのに拒否されます。end で始まる名前を自由に作れてしまうと、\end{...} が呼び出す \endname の側と衝突しかねないため、接頭辞ごと予約されているのです。この検査の詳しい話はマクロのページに譲りますが、要するに \newcommand の側の禁止事項は、いまこのページで作っている環境の名前空間を守るためにあります。
環境は自動的にグループになる——何が戻り、何が漏れるか
\begin は開始コードを走らせる 前に \begingroup を、\end は終了コードを走らせた 後に \endgroup を発行します。つまり開始コード・本文・終了コードの三つとも、まとめて 1 つのグループの中にいます。だから先ほどの warning 環境で \itshape を戻す必要が無かったのです。マクロで同じことをするなら中身を { … } で自分でくくらねばなりませんが、環境では \begin…\end がその括弧そのものになっています。書式変更を一定範囲に閉じ込めたいなら、マクロより環境のほうが素直 なのは、この一点に尽きます。
ただし「グループを抜ければ全部戻る」わけではありません。TeX の代入には局所的なものと大域的なものがあり、LaTeX のカウンタ操作は 意図的に大域 です。latex.ltx の \addtocounter は \global\advance で書かれているので、環境の中で \stepcounter した番号は \end を抜けても 1 のまま残ります。章番号や図番号が環境の内側で進んでも消えないのは、この設計のおかげです。逆に環境の中で \newcommand したマクロは \end とともに消え、外で使うと ! Undefined control sequence. になります。
| 環境の中で行うと | \end を抜けたあと | なぜ |
|---|---|---|
\itshape | 戻る | 書体の切り替えは局所的な代入 |
\setlength | 戻る | \setlength は素の(局所的な)代入 |
\newcommand | 消える | 定義は局所的。外で使うと ! Undefined control sequence. |
\stepcounter | 残る | カウンタ操作は \global で書かれている |
\gdef | 残る | 明示的に大域の定義 |
\label | 残る | .aux への書き出しはグループで取り消されない |
引数をとる環境と、既定値つきのオプション引数
呼び出しごとに中身を変えたいなら、名前のあとの角括弧に引数の個数を書き、開始コードで #1・#2 … と参照します。\newenvironment{name}[⟨個数⟩]{開始コード}{終了コード} の形で、#1 から #9 まで最大 9 個。角括弧をもう一組足して \newenvironment{name}[⟨個数⟩][⟨既定値⟩]{...}{...} とすれば #1 がオプション引数になり、\begin{name} なら既定値、\begin{name}[x] なら x が #1 に入ります。ここは \newcommand と完全に同じ書式で、⟨個数⟩ に オプション引数も数えた総数 を書く点まで共通です。
% one mandatory argument
\newenvironment{point}[1]{%
\par\noindent\textbf{#1}\quad
}{%
\par
}
\begin{point}{Conclusion}
Back up early.
\end{point}
% first argument optional, default "Note"
\newenvironment{callout}[1][Note]{%
\par\noindent\textbf{#1:}\itshape
}{%
\par
}
\begin{callout} % label is "Note"
Nothing to configure.
\end{callout}
\begin{callout}[Warning] % #1 becomes "Warning"
This cannot be undone.
\end{callout}終了コードで #1 を書くとエラーになる理由と、その回避策
引数 #1・#2 … は開始コードでしか使えません。終了コードに書くと、コンパイル時どころか 定義したその行で ! Illegal parameter number in definition of \enddemo. と叱られます。エラーが名指しする \enddemo に注目してください。ここまでの話の答え合わせになっていて、\newenvironment は引数を受け取るマクロ \demo と、引数を 一つも受け取らない マクロ \enddemo の 2 つを作るのです。パラメータを持たないマクロの本体に #1 が現れれば、TeX にとってはただの構文エラーでしかありません。「実行時に引数が消える」のではなく、最初から受け取る口が無いのです。
終了時にも引数の値が要るなら、定石は 開始コードのうちに値を保存しておく ことです。文字列なら \newsavebox で確保した箱に \sbox で詰めるのが手堅く、\def や \newcommand にマクロとして覚えさせても構いません。環境全体が 1 つのグループなので、開始コードで保存した中身は終了コードまで無事に生き延びます。次の citequote は、引用の末尾に出典を右寄せで出す例です。出典を #1(既定値 Shakespeare)で受け取り、箱 \quoteauthor に入れ、終了コードで \usebox として取り出しています。
\newsavebox{\quoteauthor}
\newenvironment{citequote}[1][Shakespeare]{%
\sbox\quoteauthor{#1}% save the argument while we still have it
\begin{quotation}%
}{%
\hspace{1em plus 1fill}---\usebox{\quoteauthor}% retrieve it here
\end{quotation}%
}
\begin{citequote}
To be, or not to be.
\end{citequote}
\begin{citequote}[Knuth]
Premature optimization is the root of all evil.
\end{citequote}\renewenvironment と、星付きの \newenvironment*
既存の環境を作り直すときは \renewenvironment を使います。引数の書き方は [⟨個数⟩][⟨既定値⟩] も含めて \newenvironment と一字一句同じで、違うのは前提だけ。\newenvironment が「まだ無いときだけ成功」するのに対し、\renewenvironment は「すでに有るときだけ成功」し、無い名前に使うと ! LaTeX Error: Environment nosuch undefined. で止まります。文書全体の quote を斜体にしたい、といった一括変更に向いています。ただしクラスやパッケージが提供する環境を作り直すと、それに依存する他のコードを巻き添えにすることがある点は忘れないでください。
% italicise every quote in the document
\renewenvironment{quote}{%
\list{}{\rightmargin\leftmargin}\item\relax\itshape
}{%
\endlist
}\newenvironment と \renewenvironment には、名前のあとに * を付けた星付き形もあります。この星が変えるのは 引数に空行を含められるかどうか です。星なしの引数は段落(\par)をまたげますが、星付きの引数は「短い」引数になり、途中に空行が混じると ! Paragraph ended before \shortenv was complete. で止まります。一見すると不便ですが、閉じ括弧を書き忘れて引数が暴走したとき、文書の末尾までではなく次の空行で止まってくれるという意味で、むしろ親切なエラーです。なお \providecommand に相当する「無いときだけ定義する」環境版は標準にはありません。それが要るなら次節の道具を使います。
\NewDocumentEnvironment なら終了コードでも引数が使える
\NewDocumentEnvironment{name}{⟨引数指定⟩}{開始コード}{終了コード} を使えば、保存ボックスの一手間は要りません。この定義法では開始コードと終了コードの 両方 が同じ引数を参照できるからです。引数の個数を数字で書く代わりに、m(必須)・o(省略可能)・O{既定値}(既定値つき)・s(星の有無)といった文字を並べた引数指定を書きます。もとは xparse パッケージの機能でしたが、2020 年 10 月 1 日のリリースでカーネルへ取り込まれ、いまは \usepackage なしで使えます(指定子の一覧は xparse のページに)。
% O{...} is an optional argument with a default; #1 works in both halves
\NewDocumentEnvironment{citequote}{O{Shakespeare}}{%
\begin{quotation}%
}{%
\hspace{1em plus 1fill}---#1%
\end{quotation}%
}
\begin{citequote}[Knuth]
Premature optimization is the root of all evil.
\end{citequote}同じリリースで \RenewDocumentEnvironment(作り直す)・\ProvideDocumentEnvironment(無いときだけ定義する)・\DeclareDocumentEnvironment(有無を問わず定義する)も揃いました。前節で「標準には無い」と書いた \providecommand の環境版が、ここでようやく手に入るわけです。新しく書くコードなら、オプション引数を複数とれること、星付きの変種を正式に扱えること、そして終了コードから引数が見えることの三点で、こちらを既定の選択にしてよいでしょう。\newenvironment は既存文書との互換性のために覚えておく、という位置づけになります。
よく使う組み立て方——書式で包む・アキを入れる・既存環境の上に載せる
実務で書く自作環境は、ほとんどが次の三つのどれかに収まります。骨格はどれも同じで、開始コードで下ごしらえをし、終了コードで後始末をするだけです。
- 書式で包む — 開始コードで書体・サイズ・寄せを設定し、本文をその書式で組む。グループ性のおかげで終了コードは空でよい。
- 前後にアキを入れる — 開始コードの先頭と終了コードの末尾に
\par\medskipなどの縦アキを置き、本文を上下の余白で囲む。 - 既存の環境の上に載せる — 開始コードで別の環境を
\begin{...}し、終了コードで対応する\end{...}を閉じる。quote・center・listを土台に少しだけ味付けする使い方。
% 1. wrap the body in formatting
\newenvironment{aside}{\par\small\itshape}{\par}
% 2. add vertical space above and below
\newenvironment{spaced}{\par\medskip\noindent}{\par\medskip}
% 3. build on an existing environment
\newenvironment{smallquote}{%
\small\begin{quotation}%
}{%
\end{quotation}%
}三つ目がいちばん出番の多い型です。土台の環境の字下げや余白はそのまま受け継がれるので、書き足す量は驚くほど少なくて済みます。ただし開始コードで開いたものは終了コードで必ず閉じ、対応を崩さないこと。それと、上の例で行末に % が並んでいるのは飾りではありません。% を書かないと行末の改行が空白 1 個として本文に紛れ込み、環境の前後に説明のつかないアキが出ます。複数行にわたる定義を書くときは、行末の % を癖にしておくと事故が減ります。
\begin と \end が食い違ったときのエラー
\end{name} は \endname を呼ぶだけでなく、いま開いている環境の名前と name が一致するかも確かめます。ここで食い違うと ! LaTeX Error: \begin{sidenote} on input line 5 ended by \end{remarkbox}. のように、開いた側の環境名と行番号 を教えてくれます。エラー行より、この行番号のほうが役に立ちます。次の 4 つが実際によく出る顔ぶれです。
! LaTeX Error: Environment nosuchenv undefined.— その名前の環境が定義されていません。綴り間違いか、定義したパッケージの\usepackage忘れです。! LaTeX Error: \begin{sidenote} on input line 5 ended by \end{remarkbox}.— 開いた名前と閉じた名前が違います。! LaTeX Error: \begin{sidenote} on input line 4 ended by \end{document}.—\end{sidenote}を書き忘れ、\end{document}まで開きっぱなしで到達しました。! LaTeX Error: \begin{document} ended by \end{nosuchenv}.— 開いていない環境を閉じました。直前の\beginが未定義でエラーになっている場合にも、続けてこの形で出ます。
いずれも「対になっていない」という一つの症状の言い換えです。自作環境で開始コードから別の環境を開いているときは、まず終了コードの \end{...} を疑ってください。そして、これらのエラーは往々にして ! Missing $ inserted. のような無関係に見える悲鳴を道連れにします。二次被害のほうが目立って見えるので、ログはいちばん最初のエラーから読む のが原則です。