Entornos personalizados (\newenvironment)

Un entorno de LaTeX no es en absoluto una construcción especial. \begin{quote} llama a un comando llamado \quote, \end{quote} llama a otro llamado \endquote, y ahí está todo el mecanismo; \newenvironment, la herramienta para definir entornos propios, se limita a escribir esas dos macros de una vez. Entendida esa pareja, lo demás viene en cadena: por qué el nombre de un entorno puede chocar con el de un comando, por qué los argumentos faltan en el código final y por qué un \begin y un \end desparejados producen justo ese mensaje de error. Esta página sigue ese hilo por \newenvironment y \renewenvironment, los argumentos y el argumento opcional, la agrupación que se obtiene gratis y el moderno \NewDocumentEnvironment.

Cómo escribir \newenvironment: código inicial y código final

Basta con poner \newenvironment{name}{código inicial}{código final} en el preámbulo para que \begin{name}…\end{name} quede disponible en el cuerpo. El primer argumento es el nombre del entorno, escrito sin barra invertida; el segundo es el código que se ejecuta cuando LaTeX encuentra \begin{name}, y el tercero el que se ejecuta al llegar a \end{name}. El cuerpo atrapado entre ambos no se toca en absoluto: se compone de la forma habitual. Diseñar un entorno propio se reduce, pues, a una sola pregunta: qué se prepara al entrar y qué se recoge al salir.

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}

Que el código final se reduzca aquí a un solo \par no es pereza. Nada desactiva el \itshape (cursiva) encendido en el código inicial y, aun así, el texto posterior al entorno vuelve a salir redondo. Las dos secciones siguientes lo explican; en pocas palabras, un entorno forma automáticamente un grupo. El truco al escribir \newenvironment está en no deshacer lo que se deshace solo: un {} vacío es un código final perfectamente válido.

Qué es realmente un entorno: el par de macros \name y \endname

\begin{name} llama a \name y \end{name} llama a \endname. No es una conjetura: la definición de \end en latex.ltx invoca literalmente \csname end#1\endcsname. Quien prefiera verlo a creerlo tiene \show. Sobre el entorno estándar quote, \show\quote y \show\endquote muestran, por un lado, una macro que abre una \list y, por otro, una macro que no es más que \endlist. No hay en ninguna parte una sintaxis propia de los entornos: solo dos macros emparejadas por el nombre.

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 consecuencia aparece casi de inmediato, en forma de error. El nombre de un entorno ocupa el nombre de comando homógrafo. Pruebe \newenvironment{alpha}{...}{...} y la compilación se detiene con ! LaTeX Error: Command \alpha already defined., porque la letra griega \alpha ya existe. Los nombres de entorno y los de comando nunca fueron dos espacios de nombres, sino uno solo. \newenvironment{quote} falla con Command \quote already defined. por idéntico motivo, y la comprobación es deliberada: impide pisar algo sin darse cuenta. Nombres como mywarning o thmbox, difíciles de hacer chocar con un comando existente, son la costumbre segura.

La cara opuesta de la misma lógica es una restricción conocida del lado de \newcommand: \newcommand{\endnotes}{...} se rechaza aunque no exista tal comando en ninguna parte. Si cualquiera pudiera inventar nombres que empiecen por end, podrían chocar con la mitad \endname a la que \end{...} despacha, así que todo el prefijo está reservado. El detalle de esa comprobación corresponde a la página de macros; lo relevante aquí es que aquella prohibición protege justamente el espacio de nombres con el que se construyen los entornos de esta página.

Los entornos son grupos automáticos: qué se revierte y qué se escapa

Antes de ejecutar el código inicial, \begin emite un \begingroup; después del código final, \end emite el \endgroup correspondiente. Código inicial, cuerpo y código final quedan, pues, dentro de un único grupo. Por eso el entorno warning de más arriba nunca tuvo que apagar \itshape. Hacer lo mismo con una macro obliga a envolver el contenido en { … } a mano; en un entorno, \begin\end es ese par de llaves. Cuando un cambio de formato debe quedarse dentro de un tramo fijo, el entorno es la herramienta más honesta que la macro, y toda la razón está aquí.

Eso no significa que todo se revierta al cerrar el grupo. Las asignaciones de TeX son locales o globales, y LaTeX hace las operaciones sobre contadores deliberadamente globales: \addtocounter está escrito en latex.ltx con \global\advance, de modo que un contador incrementado con \stepcounter dentro de un entorno sigue incrementado después de \end. Gracias a ese diseño, un número de sección o de figura avanzado dentro de un entorno no se evapora. En cambio, una macro definida con \newcommand ahí dentro muere con \end, y usarla fuera produce ! Undefined control sequence.

Ejecutado dentro del entornoDespués de \endPor qué
\itshapese revierteun cambio de fuente es una asignación local
\setlengthse revierte\setlength es una asignación local corriente
\newcommanddesaparecela definición es local; fuera aparece ! Undefined control sequence.
\stepcounterpersistelas operaciones sobre contadores usan \global
\gdefpersistedefinición explícitamente global
\labelpersisteuna escritura en el archivo .aux no la deshace un grupo

Entornos con argumentos y un argumento opcional con valor por defecto

Para variar el contenido de una llamada a otra, se escribe el número de argumentos entre corchetes tras el nombre y se accede a ellos en el código inicial como #1, #2, etc.: \newenvironment{name}[⟨n⟩]{código inicial}{código final}, de #1 a #9, nueve como máximo. Un segundo par de corchetes —\newenvironment{name}[⟨n⟩][⟨defecto⟩]{...}{...}— vuelve opcional a #1: \begin{name} toma el valor por defecto y \begin{name}[x] pone x en #1. La sintaxis es idéntica a la de \newcommand, incluida la regla de que ⟨n⟩ es el total, contando el argumento opcional.

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}

Por qué #1 en el código final da error y cómo sortearlo

Los argumentos #1, #2, … solo pueden usarse en el código inicial. Si uno aparece en el código final, LaTeX protesta no al usarlo, sino en la misma línea que lo define: ! Illegal parameter number in definition of \enddemo. Fíjese en el nombre que señala el error, \enddemo: confirma todo lo anterior, porque \newenvironment fabrica una macro \demo que recibe argumentos y una macro \enddemo que no recibe ninguno. Un #1 en el cuerpo de una macro sin texto de parámetros es, para TeX, un simple error de sintaxis. Los argumentos no desaparecen en tiempo de ejecución: nunca hubo un hueco donde recibirlos.

Si de verdad hace falta el valor de un argumento al cerrar, la maniobra estándar es guardarlo mientras se sigue en el código inicial. Para texto, la opción sólida es una caja: se reserva con \newsavebox y se rellena con \sbox. También sirve que una macro lo recuerde mediante \def o \newcommand. Como todo el entorno es un grupo, lo guardado en el código inicial llega intacto al código final. El citequote de abajo compone la fuente alineada a la derecha al terminar la cita: recibe la fuente como #1 (por defecto Shakespeare), la mete en la caja \quoteauthor y la saca con \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 y la forma con estrella \newenvironment*

Para rehacer un entorno que ya existe se emplea \renewenvironment. Su sintaxis de argumentos, incluido [⟨n⟩][⟨defecto⟩], es palabra por palabra la de \newenvironment; solo cambia la condición previa. Mientras \newenvironment solo tiene éxito si el nombre está libre, \renewenvironment solo lo tiene si está ocupado, y aplicarlo a un nombre inexistente se detiene con ! LaTeX Error: Environment nosuch undefined. Sirve para cambios de conjunto, como poner en cursiva todos los quote de un documento. Conviene recordar, eso sí, que redefinir un entorno provisto por una clase o un paquete puede arrastrar consigo cualquier código que dependa de él.

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

Tanto \newenvironment como \renewenvironment tienen además una forma con estrella, escrita con * tras el nombre. Lo que la estrella cambia es si un argumento puede contener una línea en blanco. Sin estrella, un argumento puede abarcar párrafos (\par); con estrella se convierte en un argumento corto, y una línea en blanco dentro detiene la compilación con ! Paragraph ended before \shortenv was complete. Parece una restricción, pero es un favor: cuando una llave de cierre olvidada hace que un argumento se desboque, la estrella lo frena en la siguiente línea en blanco en lugar de al final del documento. Por lo demás, no existe un equivalente estándar de \providecommand para entornos; para eso está la herramienta de la sección siguiente.

\NewDocumentEnvironment: argumentos que el código final sí puede ver

Con \NewDocumentEnvironment{name}{⟨arg-spec⟩}{código inicial}{código final} el rodeo por la caja de guardado sobra, porque en esta interfaz ambas mitades acceden a los mismos argumentos. En lugar de un número se escribe una especificación de argumentos: letras como m (obligatorio), o (opcional), O{defecto} (opcional con valor por defecto) y s (estrella facultativa). Nació en el paquete xparse, pero la versión del 1 de octubre de 2020 lo incorporó al núcleo de LaTeX, así que hoy funciona sin ningún \usepackage (la página de xparse enumera los especificadores).

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 misma versión trajo \RenewDocumentEnvironment (rehacer), \ProvideDocumentEnvironment (definir solo si falta) y \DeclareDocumentEnvironment (definir en cualquier caso). El equivalente de \providecommand para entornos, ausente en la serie clásica como se vio antes, llega por fin aquí. Para código nuevo, esta familia merece ser la opción por defecto por tres motivos: admite varios argumentos opcionales, maneja bien las variantes con estrella y su código final ve los argumentos. \newenvironment queda entonces sobre todo para leer y mantener documentos ya existentes.

Tres patrones que cubren casi todos los entornos propios

Casi todos los entornos que se escriben en la práctica caen en uno de tres patrones. El esqueleto es siempre el mismo: preparar en el código inicial y recoger en el código final.

  • Envolver el cuerpo en formato — fijar fuente, tamaño o alineación en el código inicial y dejar que el cuerpo adopte ese aspecto. Gracias a la agrupación, el código final puede quedar vacío.
  • Añadir espacio arriba y abajo — poner espacio vertical como \par\medskip al principio del código inicial y al final del código final, enmarcando el cuerpo con márgenes.
  • Construir sobre un entorno existente — abrir otro entorno con \begin{...} en el código inicial y cerrar el \end{...} correspondiente en el código final. Así se sazona quote, center o list con algo de más.
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}%
}

El tercer patrón es el más socorrido. El entorno subyacente conserva su sangría y sus márgenes, de modo que lo que hay que añadir resulta sorprendentemente poco. Solo hay que cerrar en el código final todo lo que se abrió en el inicial, sin romper el emparejamiento. Y los % al final de línea de los ejemplos anteriores no son adorno: sin ellos, el salto de línea entra en el texto como un espacio y aparecen huecos inexplicables alrededor del entorno. Convertir el % final en costumbre al escribir definiciones de varias líneas elimina toda una categoría de accidentes.

Los errores cuando \begin y \end no coinciden

\end{name} no se limita a llamar a \endname: comprueba también que name coincida con el entorno abierto en ese momento. Cuando no cuadran aparece, por ejemplo, ! LaTeX Error: \begin{sidenote} on input line 5 ended by \end{remarkbox}., que informa de el nombre del entorno abierto y la línea donde se abrió. Ese número de línea suele ser más útil que la línea en la que se avisa del error. Estas cuatro son las caras que uno encuentra de verdad.

  • ! LaTeX Error: Environment nosuchenv undefined. — no existe ningún entorno con ese nombre. O es una errata, o falta el \usepackage del paquete que lo define.
  • ! LaTeX Error: \begin{sidenote} on input line 5 ended by \end{remarkbox}. — el nombre abierto y el cerrado no coinciden.
  • ! LaTeX Error: \begin{sidenote} on input line 4 ended by \end{document}. — falta un \end{sidenote}, así que el entorno siguió abierto hasta \end{document}.
  • ! LaTeX Error: \begin{document} ended by \end{nosuchenv}. — se ha cerrado un entorno que nunca se abrió. Este mensaje también sigue de inmediato a un \begin indefinido que ya provocó su propio error.

Las cuatro reformulan un mismo síntoma: algo no está emparejado. Si el entorno propio abre otro entorno en su código inicial, el primer sospechoso es el \end{...} de su código final. Conviene saber, además, que estos errores suelen arrastrar chillidos en apariencia ajenos, como ! Missing $ inserted. El daño colateral resulta más ruidoso que la causa, así que la regla es leer el registro desde el primerísimo error.