環境の自作(\newenvironment)

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} に出会ったときに実行されるコードです。挟まれた本文そのものには手が加わらず、ふつうに組まれます。つまり自作環境の設計とは、「入口で何を仕込み、出口で何を片づけるか」を決めることに尽きます。

latex
% 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 つのマクロだけです。

latex
% 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. になるのも同じ理由で、これは事故防止のための意図的な検査です。自作環境の名前は mywarningthmbox のように、既存コマンドと衝突しにくい語を選ぶのが安全です。

同じ理屈の裏返しが、\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 と完全に同じ書式で、⟨個数⟩オプション引数も数えた総数 を書く点まで共通です。

latex
% 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 として取り出しています。

latex
\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 を斜体にしたい、といった一括変更に向いています。ただしクラスやパッケージが提供する環境を作り直すと、それに依存する他のコードを巻き添えにすることがある点は忘れないでください。

latex
% 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 のページに)。

latex
% 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{...} を閉じる。quotecenterlist を土台に少しだけ味付けする使い方。
latex
% 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. のような無関係に見える悲鳴を道連れにします。二次被害のほうが目立って見えるので、ログはいちばん最初のエラーから読む のが原則です。