Environnements personnalisés (\newenvironment)

Un environnement LaTeX n’est pas un objet particulier. \begin{quote} appelle une commande nommée \quote, \end{quote} en appelle une nommée \endquote, et tout le mécanisme tient là ; \newenvironment, l’outil qui sert à définir ses propres environnements, ne fait qu’écrire ces deux macros d’un seul geste. Une fois cette paire comprise, le reste s’enchaîne : pourquoi un nom d’environnement peut entrer en collision avec un nom de commande, pourquoi les arguments manquent dans le code de fin, et pourquoi un \begin et un \end mal appariés donnent exactement ce message d’erreur. Cette page suit ce fil à travers \newenvironment et \renewenvironment, les arguments et l’argument optionnel, le groupement offert au passage, jusqu’au moderne \NewDocumentEnvironment.

Écrire \newenvironment : code de début et code de fin

Il suffit de placer \newenvironment{name}{code de début}{code de fin} dans le préambule pour disposer de \begin{name}…\end{name} dans le corps du document. Le premier argument est le nom de l’environnement, écrit sans antislash ; le deuxième est le code exécuté lorsque LaTeX rencontre \begin{name}, le troisième celui exécuté à \end{name}. Le corps pris entre les deux n’est pas modifié : il est composé normalement. Concevoir son propre environnement revient donc à une seule question : que met-on en place à l’entrée, et que range-t-on à la sortie ?

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}

Si le code de fin se réduit ici à un seul \par, ce n’est pas par paresse. Rien ne désactive le \itshape (italique) enclenché dans le code de début, et pourtant le texte qui suit l’environnement revient au romain. Les deux sections suivantes l’expliquent en détail ; en un mot, un environnement forme automatiquement un groupe. L’astuce, en écrivant \newenvironment, consiste à ne pas défaire ce qui se défait tout seul : un {} vide fait un très bon code de fin.

Ce qu’est vraiment un environnement : la paire \name et \endname

\begin{name} appelle \name et \end{name} appelle \endname. Ce n’est pas une supposition : la définition de \end dans latex.ltx invoque littéralement \csname end#1\endcsname. Pour le vérifier plutôt que le croire, TeX propose \show. Sur l’environnement standard quote, \show\quote et \show\endquote révèlent d’un côté une macro qui ouvre une \list, de l’autre une macro qui n’est rien de plus que \endlist. Il n’existe nulle part de syntaxe propre aux environnements : seulement deux macros appariées par leur nom.

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

La conséquence se manifeste presque aussitôt, sous forme d’erreur. Un nom d’environnement occupe le nom de commande homographe. Essayez \newenvironment{alpha}{...}{...} : la compilation s’arrête sur ! LaTeX Error: Command \alpha already defined., puisque la lettre grecque \alpha existe déjà. Les noms d’environnements et les noms de commandes n’ont jamais formé deux espaces de noms, mais un seul. \newenvironment{quote} échoue sur Command \quote already defined. pour la même raison, et ce contrôle est volontaire : il empêche d’écraser quelque chose par inadvertance. Des noms comme mywarning ou thmbox, peu susceptibles d’entrer en collision, constituent l’habitude prudente.

Le revers de la même logique est une restriction bien connue du côté de \newcommand : \newcommand{\endnotes}{...} est refusé alors qu’aucune commande de ce nom n’existe. Si chacun pouvait inventer des noms commençant par end, ils risqueraient d’entrer en collision avec la moitié \endname vers laquelle \end{...} renvoie ; tout le préfixe est donc réservé. Le détail de ce contrôle appartient à la page des macros ; ce qui compte ici, c’est que cette interdiction protège précisément l’espace de noms dont sont faits les environnements de cette page.

Un environnement forme un groupe automatique : ce qui se rétablit et ce qui s’échappe

Avant d’exécuter le code de début, \begin émet un \begingroup ; après le code de fin, \end émet le \endgroup correspondant. Code de début, corps et code de fin se trouvent donc tous dans un seul groupe. C’est pourquoi l’environnement warning ci-dessus n’a jamais eu à désactiver \itshape. Avec une macro, il faudrait entourer soi-même le contenu de { … } ; dans un environnement, \begin\end est cette paire d’accolades. Pour confiner un changement de mise en forme à une zone déterminée, l’environnement est l’outil le plus franc, et toute la raison tient là.

Cela ne signifie pas pour autant que tout se rétablisse à la fermeture du groupe. Les affectations de TeX sont locales ou globales, et LaTeX rend les opérations sur les compteurs volontairement globales : \addtocounter est écrit dans latex.ltx avec \global\advance, si bien qu’un compteur incrémenté par \stepcounter à l’intérieur d’un environnement reste incrémenté après \end. C’est grâce à ce choix qu’un numéro de section ou de figure avancé dans un environnement ne s’évapore pas. À l’inverse, une macro définie par \newcommand à l’intérieur meurt avec \end ; l’employer ensuite donne ! Undefined control sequence.

Exécuté dans l’environnementAprès \endPourquoi
\itshapese rétablitun changement de police est une affectation locale
\setlengthse rétablit\setlength est une affectation locale ordinaire
\newcommanddisparaîtla définition est locale ; à l’extérieur, ! Undefined control sequence.
\stepcounterpersisteles opérations sur compteurs sont écrites avec \global
\gdefpersistedéfinition explicitement globale
\labelpersisteune écriture dans le fichier .aux n’est pas annulée par un groupe

Environnements à arguments et argument optionnel avec valeur par défaut

Pour faire varier le contenu d’un appel à l’autre, on écrit le nombre d’arguments entre crochets après le nom et on y accède dans le code de début par #1, #2, etc. : \newenvironment{name}[⟨n⟩]{code de début}{code de fin}, de #1 à #9, neuf au maximum. Une seconde paire de crochets — \newenvironment{name}[⟨n⟩][⟨défaut⟩]{...}{...} — rend #1 optionnel : \begin{name} prend la valeur par défaut, \begin{name}[x] place x dans #1. La syntaxe est identique à celle de \newcommand, jusqu’à la règle voulant que ⟨n⟩ soit le total, argument optionnel compris.

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}

Pourquoi #1 dans le code de fin provoque une erreur, et comment contourner

Les arguments #1, #2, … ne s’emploient que dans le code de début. Placé dans le code de fin, l’un d’eux fait protester LaTeX non pas à l’usage mais dès la ligne qui le définit : ! Illegal parameter number in definition of \enddemo. Notez le nom que l’erreur désigne, \enddemo : il confirme tout ce qui précède, car \newenvironment fabrique une macro \demo qui reçoit des arguments et une macro \enddemo qui n’en reçoit aucun. Un #1 dans le corps d’une macro sans texte de paramètre n’est, pour TeX, qu’une erreur de syntaxe. Les arguments ne disparaissent pas à l’exécution : il n’y a jamais eu d’emplacement pour les recevoir.

Si la valeur d’un argument est vraiment nécessaire à la fermeture, le geste classique consiste à la sauvegarder tant qu’on est encore dans le code de début. Pour du texte, la boîte est le choix solide : en réserver une avec \newsavebox et la remplir avec \sbox. Faire mémoriser la valeur par une macro via \def ou \newcommand fonctionne également. Comme l’environnement entier forme un groupe, ce qui a été enregistré au début parvient intact au code de fin. Le citequote ci-dessous compose la source alignée à droite en fin de citation : il reçoit la source en #1 (défaut Shakespeare), la range dans la boîte \quoteauthor et la ressort avec \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 et la forme étoilée \newenvironment*

Pour remanier un environnement déjà existant, on emploie \renewenvironment. Sa syntaxe d’arguments, [⟨n⟩][⟨défaut⟩] compris, est mot pour mot celle de \newenvironment ; seule la condition préalable change. Là où \newenvironment ne réussit que si le nom est libre, \renewenvironment ne réussit que s’il est pris, et l’appliquer à un nom inexistant s’arrête sur ! LaTeX Error: Environment nosuch undefined. Cela convient aux modifications d’ensemble, par exemple mettre en italique tous les quote d’un document. Il faut néanmoins se rappeler que redéfinir un environnement fourni par une classe ou un package peut entraîner avec lui tout code qui en dépend.

latex
% italicise every quote in the document
\renewenvironment{quote}{%
  \list{}{\rightmargin\leftmargin}\item\relax\itshape
}{%
  \endlist
}

\newenvironment comme \renewenvironment possèdent aussi une forme étoilée, avec un * après le nom. Ce que l’étoile change, c’est la possibilité de mettre une ligne vide dans un argument. Sans étoile, un argument peut enjamber les paragraphes (\par) ; avec étoile, il devient un argument court, et une ligne vide à l’intérieur arrête la compilation sur ! Paragraph ended before \shortenv was complete. Cela ressemble à une contrainte, mais c’est une faveur : lorsqu’une accolade fermante oubliée fait s’emballer un argument, l’étoile le stoppe à la prochaine ligne vide plutôt qu’à la fin du document. À noter enfin qu’il n’existe pas d’équivalent standard de \providecommand pour les environnements ; pour cela, il faut l’outil de la section suivante.

\NewDocumentEnvironment : des arguments visibles jusque dans le code de fin

Avec \NewDocumentEnvironment{name}{⟨arg-spec⟩}{code de début}{code de fin}, le détour par la boîte de sauvegarde devient inutile : dans cette interface, les deux moitiés accèdent aux mêmes arguments. Au lieu d’un nombre, on écrit une spécification d’arguments, c’est-à-dire des lettres comme m (obligatoire), o (optionnel), O{défaut} (optionnel avec valeur par défaut) et s (étoile facultative). L’ensemble venait à l’origine du package xparse, mais la version du 1er octobre 2020 l’a intégré au noyau de LaTeX ; il fonctionne donc désormais sans aucun \usepackage (la page xparse recense les spécificateurs).

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}

La même version a apporté \RenewDocumentEnvironment (refaire), \ProvideDocumentEnvironment (définir seulement si absent) et \DeclareDocumentEnvironment (définir dans tous les cas). L’équivalent de \providecommand pour les environnements, absent de la série classique comme on l’a vu, arrive enfin ici. Pour du code neuf, cette famille mérite d’être le choix par défaut pour trois raisons : plusieurs arguments optionnels, un traitement propre des variantes étoilées, et un code de fin qui voit les arguments. \newenvironment reste alors surtout utile pour lire et maintenir les documents existants.

Trois schémas qui couvrent la plupart des environnements maison

Presque tous les environnements écrits en pratique relèvent de l’un de trois schémas. L’ossature est partout la même : préparer dans le code de début, ranger dans le code de fin.

  • Envelopper le corps dans une mise en forme — régler police, taille ou alignement dans le code de début et laisser le corps prendre cet aspect. Grâce au groupement, le code de fin peut rester vide.
  • Ajouter de l’espace au-dessus et au-dessous — placer un espace vertical comme \par\medskip en tête du code de début et en queue du code de fin, encadrant le corps de marges.
  • S’appuyer sur un environnement existant — ouvrir un autre environnement avec \begin{...} dans le code de début et fermer le \end{...} correspondant dans le code de fin. C’est ainsi qu’on relève quote, center ou list d’une touche supplémentaire.
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}%
}

Le troisième schéma est celui auquel on recourt le plus. L’environnement sous-jacent conserve son indentation et ses marges, si bien que la quantité à ajouter est étonnamment faible. Il faut seulement refermer dans le code de fin tout ce que le code de début a ouvert, sans rompre l’appariement. Et les % en fin de ligne des exemples ci-dessus ne sont pas décoratifs : sans eux, le passage à la ligne entre dans le texte sous forme d’une espace, et des trous inexpliqués apparaissent autour de l’environnement. Prendre l’habitude du % final dans les définitions multilignes supprime toute une catégorie d’accidents.

Les erreurs quand \begin et \end ne correspondent pas

\end{name} ne se contente pas d’appeler \endname : il vérifie aussi que name correspond à l’environnement actuellement ouvert. En cas de désaccord, on obtient par exemple ! LaTeX Error: \begin{sidenote} on input line 5 ended by \end{remarkbox}., qui indique le nom de l’environnement ouvert et la ligne où il l’a été. Ce numéro de ligne est en général plus utile que celui où l’erreur est signalée. Voici les quatre visages que l’on croise vraiment.

  • ! LaTeX Error: Environment nosuchenv undefined. — aucun environnement de ce nom n’existe. Soit une faute de frappe, soit un \usepackage oublié pour le package qui le définit.
  • ! LaTeX Error: \begin{sidenote} on input line 5 ended by \end{remarkbox}. — le nom ouvert et le nom fermé diffèrent.
  • ! LaTeX Error: \begin{sidenote} on input line 4 ended by \end{document}. — un \end{sidenote} manque, l’environnement est resté ouvert jusqu’à \end{document}.
  • ! LaTeX Error: \begin{document} ended by \end{nosuchenv}. — un environnement jamais ouvert a été fermé. Ce message suit aussi immédiatement un \begin indéfini qui a déjà provoqué sa propre erreur.

Les quatre reformulent un seul symptôme : quelque chose n’est pas apparié. Si l’environnement maison ouvre un autre environnement dans son code de début, le premier suspect est le \end{...} de son code de fin. Il faut aussi savoir que ces erreurs entraînent souvent des cris apparemment sans rapport, comme ! Missing $ inserted. Les dégâts collatéraux sont plus bruyants que la cause : la règle est donc de lire le journal à partir de la toute première erreur.