Ayudas de programación

A Philipp Lehman se le conoce sobre todo por biblatex y csquotes, pero la obra suya que funciona en silencio en más preámbulos es probablemente la tercera: etoolbox. La razón se reduce casi a una sola orden: \patchcmd, que sustituye solo una parte de la macro de otra persona en lugar de redefinirla entera. Hay una trampa, eso sí: si no encuentra el texto buscado, \patchcmd no hace absolutamente nada, ni error ni aviso. Eso suele estar detrás de un retoque de preámbulo que «deja de funcionar» misteriosamente al día siguiente de actualizar un paquete. Esta página cubre las pruebas, banderas, hooks y parches de etoolbox, luego pgfkeys, el motor con el que los paquetes LaTeX construyen interfaces key=value, y por último \fpeval para la aritmética con reales.

Qué es etoolbox: una caja de herramientas de e-TeX con cara de LaTeX

etoolbox es una caja de herramientas de programación para quien escribe clases y paquetes. Reenvuelve las primitivas de bajo nivel que añadió e-TeX para que se sientan como LaTeX2e, y encima suma un buen surtido de comodidades generales. La versión que trae TeX Live 2024 es la 2.5k, del 5 de octubre de 2020, y el aviso de copyright lleva dos nombres: Philipp Lehman (2007–2011) y Joseph Wright (2015–2020). Todo motor TeX moderno incluye e-TeX, así que basta con \usepackage{etoolbox}. Incluso ahora que expl3, la capa de programación de LaTeX3, está extendida, etoolbox sobrevive porque encaja directamente en el mundo LaTeX2e: los argumentos se escriben #1, la ramificación es el familiar par {verdadero}{falso} y, sobre todo, está \patchcmd para arreglar después el paquete ajeno. Para trabajo real de preámbulo, la combinación es difícil de superar.

Escribir pruebas: \ifdef, \ifdefempty, \ifstrequal

Todas las pruebas de etoolbox tienen la misma forma: un par final {⟨código si es cierto⟩}{⟨código si es falso⟩}. Ningún \fi que recordar, ninguna duda sobre dónde va el \else. «¿Está ya definida esta orden?» se escribe \ifdef{\cmd}{cierto}{falso}, o \ifcsdef{name}{cierto}{falso} si se tiene el nombre como cadena (con \ifundef e \ifcsundef como negaciones). Para cadenas: \ifblank para «¿son solo espacios?», su negación \notblank, \ifstrequal{cadena}{cadena}{cierto}{falso} para la igualdad de dos cadenas, \ifdefempty{\cmd}{cierto}{falso} para «¿está vacío el cuerpo de esta macro?» e \ifstrempty{cadena}{cierto}{falso} para «¿está vacía la cadena misma?». No los confunda con el parecido \ifdefined, que es una primitiva de e-TeX y no una bifurcación de dos vías de etoolbox.

latex
\usepackage{etoolbox}

% provide a command only if nobody defined it yet
\ifdef{\highlight}
  {}                                    % already there: leave it alone
  {\newcommand{\highlight}[1]{\textbf{#1}}}

% behave differently on an empty argument
\newcommand{\field}[1]{\ifblank{#1}{(none)}{#1}}

% numeric tests, same two-way shape
\ifnumcomp{\value{page}}{>}{10}{late}{early}
\ifnumodd{\value{page}}{recto}{verso}

Hay aquí una distinción que hasta la documentación pone fácil pasar por alto: \ifstrequal e \ifdefstring no son expansibles. Mire el código fuente de etoolbox y encontrará ambos definidos con \newrobustcmd, es decir, con el prefijo \protected de e-TeX, por lo que no se comportan como se espera dentro de \edef, \typeout o \csname. Escriba \typeout{\ifstrequal{abc}{abc}{SAME}{DIFF}} y el registro muestra no SAME sino literalmente \ifstrequal {abc}{abc}{SAME}{DIFF}. \ifdefempty, en cambio, sí es expansible y dentro de un \edef deja solo el resultado. Todos ramifican bien en el cuerpo del documento; la diferencia solo aparece dentro de un \edef: saber dónde cae esa línea evita perder un día persiguiendo una prueba que «no funciona».

Banderas booleanas: ¿\newtoggle o \newbool?

La elección por defecto debe ser \newtoggle, y la razón es el espacio de nombres: un toggle vive en su propio espacio de nombres, así que nunca puede chocar con una orden existente. Se declara con \newtoggle{draft}, se conmuta con \toggletrue{draft} / \togglefalse{draft} (o \settoggle{draft}{true}), se ramifica con \iftoggle{draft}{⟨cierto⟩}{⟨falso⟩} y se invierte con \nottoggle. La otra familia, el bool, ofrece la misma forma —\newbool{draft}, \setbool{draft}{true}, \booltrue, \boolfalse, \ifbool{draft}{⟨cierto⟩}{⟨falso⟩}— pero por dentro usa la misma maquinaria que el \newif de LaTeX, de modo que consume un nombre de orden, \ifdraft. Ese es el punto decisivo: elija un bool cuando necesite interoperar con código existente basado en \newif, y un toggle para todo lo demás.

ComandoSignificadoNota
\newtoggle{f}Declara la bandera f, falsa al inicioEspacio de nombres propio; no consume nombres de orden
\settoggle{f}{v}Pone f a v (true / false)Equivale a \toggletrue / \togglefalse
\iftoggle{f}{T}{F}T si es cierto, F si es falsoTres argumentos; no hace falta \fi
\newbool{f}La versión bool de una banderaMisma maquinaria que \newif; reserva un nombre de orden
\ifbool{f}{T}{F}La versión bool de la bifurcaciónSe lleva bien con el código existente basado en \newif

\newrobustcmd y \robustify: una macro que no se rompe

\newrobustcmd se escribe exactamente como \newcommand pero produce una orden robusta. La diferencia se ve al instante con \meaning: una orden creada con \newcommand informa \long macro:->…, mientras que una creada con \newrobustcmd informa \protected\long macro:->…. Es decir, se salta el baile en dos tiempos del \protect tradicional y usa directamente el prefijo \protected de e-TeX. Por eso puede aparecer dentro de un argumento móvil —un encabezado, un pie de figura— sin que se expanda y se rompa camino del archivo de índice. Para una orden frágil que ya definió otra persona, \robustify{\cmd} endurece la definición existente en su sitio.

\patchcmd: sustituir solo una parte de la macro ajena

\patchcmd localiza una cadena de búsqueda dentro del cuerpo de una macro ya definida y sustituye solo eso. Toma cinco argumentos: \patchcmd{\cmd}{⟨buscar⟩}{⟨sustituir⟩}{⟨si acierta⟩}{⟨si falla⟩}. Si encuentra el texto buscado, sustituye y ejecuta el cuarto argumento; si no, deja la macro intacta y ejecuta el quinto. Solo se sustituye la primera aparición: con dos \small en el cuerpo, solo cambia el anterior. He aquí un caso realmente útil. El entorno thebibliography de la clase article abre con \section*{\refname}, así que sustituir ese \section* por \section convierte la bibliografía en una sección numerada que además aparece en el índice. Medido, el archivo .toc recibió debidamente \contentsline {section}{\numberline {2}References}, y el parche cumplió lo prometido.

document.tex
\usepackage{etoolbox}

\makeatletter                    % the target usually contains @
\patchcmd{\thebibliography}
  {\section*}                    % search
  {\section}                     % replace
  {\typeout{bibliography patch applied}}                        % on success
  {\PackageWarning{mypkg}{bibliography patch failed}}           % on failure
\makeatother

% result: "References" becomes a numbered section and enters the ToC
%   .toc -> \contentsline {section}{\numberline {2}References}{1}{}

Cuando un parche no hace nada en silencio: \tracingpatches y xpatch

Un \patchcmd fallido es totalmente mudo. Medido: dele un patrón que no case y deje ambas ramas vacías, y la compilación termina con cero errores y cero avisos, sin dejar rastro en el registro. De ahí la regla de hierro: nunca deje vacía la rama de fallo; ponga en ella un \PackageWarning. Entonces obtendrá Package mypkg Warning: bibliography patch failed on input line 5. y se enterará al día siguiente de la actualización en lugar de meses después. Para averiguar por qué, coloque \tracingpatches en el preámbulo: se carga etoolbox.def y se escribe en el registro un diagnóstico por cada parche.

log
[debug] tracing \patchcmd on input line 5
[debug] analyzing '\thebibliography'
[debug] ++ control sequence is defined
[debug] ++ control sequence is a macro
[debug] ++ macro can be retokenized cleanly
[debug] -- search pattern not found in replacement text

[debug] analyzing '\nosuchcommand'
[debug] -- control sequence is undefined or \relax

[debug] analyzing '\LaTeX'
[debug] -- macro cannot be retokenized cleanly
[debug] -> the macro may have been defined under a category
[debug]    code regime different from the current one

Los diagnósticos se reparten en tres clases. «El patrón buscado no está en el cuerpo» (-- search pattern not found in replacement text) es la señal clásica de que una actualización del paquete cambió la definición; inspeccione la nueva con \show y reescriba su texto de búsqueda. «La orden no está definida» (-- control sequence is undefined or \relax) significa que está parcheando demasiado pronto: retrase el parche, por ejemplo con \AtBeginDocument. El tercero, «no puede retokenizarse limpiamente» (-- macro cannot be retokenized cleanly), es un problema de código de categoría: la macro se definió bajo un régimen de catcodes distinto del actual, así que compruebe que está parcheando dentro de \makeatletter.

Y hay un fallo que ni siquiera aparece en el diagnóstico: \patchcmd no funciona sobre una orden con argumento opcional. Pregunte a \meaning por un \opt definido como \newcommand{\opt}[2][X]{...} y obtendrá \@protected@testopt \opt \\opt {X}: \opt es solo una puerta de entrada que despacha, y el cuerpo real vive en otra orden llamada \\opt. Así que \patchcmd{\opt}{small}{LARGE} rebusca en la puerta y falla. Para ese caso use \xpatchcmd del paquete xpatch, que extiende etoolbox: medido, los mismos argumentos tuvieron éxito y la macro interna quedó como \long macro:[#1]#2-><#1|#2|LARGE>. xpatch también trae las órdenes equivalentes para entornos.

Hooks, añadidos y listas: colar tu código en el de otros

Si se puede evitar reescribir el cuerpo de una macro, mejor evitarlo. etoolbox ofrece un generoso surtido de hooks del tipo «ejecuta este código en tal momento». El principio y el final del documento corresponden al núcleo de LaTeX con \AtBeginDocument y \AtEndDocument, pero etoolbox añade \AtEndPreamble (el final mismo del preámbulo), \AfterEndDocument (de verdad el último) y hooks alrededor de un entorno concreto: \AtBeginEnvironment{⟨env⟩}{⟨código⟩}, \AtEndEnvironment, \BeforeBeginEnvironment y \AfterEndEnvironment. Para añadir después a una macro o hook existente están \appto{\cmd}{⟨código⟩} (al final) y \preto{\cmd}{⟨código⟩} (al principio); \gappto es la variante global y \eappto expande antes el código añadido. Para una macro con argumentos se usan \apptocmd / \pretocmd, con ramas de acierto y fallo; estas también se limitan a ejecutar la rama de fallo ante una orden indefinida, sin error, así que exigen la misma cautela que \patchcmd.

latex
\usepackage{etoolbox}

% run code every time an environment starts -- no patching required
\AtBeginEnvironment{quote}{\itshape}
\AtBeginEnvironment{itemize}{\setlength{\itemsep}{2pt}}

% append to a macro that takes an argument (note the two branches)
\newcommand{\greet}[1]{Hello #1}
\apptocmd{\greet}{!}{}{\PackageWarning{mypkg}{could not extend \string\greet}}
% \greet is now  \long macro:#1->Hello #1!

% lightweight lists and loops
\listadd{\mylist}{alpha}\listadd{\mylist}{beta}
\newcommand{\asitem}[1]{\item #1}
\begin{itemize}\forlistloop{\asitem}{\mylist}\end{itemize}
\begin{itemize}\forcsvlist{\asitem}{apples, pears, plums}\end{itemize}

El lado de las listas también está cubierto. \listadd{\mylist}{⟨elemento⟩} añade a una lista interna, y \forlistloop{⟨manejador⟩}{\mylist} aplica un manejador de un argumento a cada elemento. Si ya se tiene una cadena separada por comas, \docsvlist{a,b,c} y \forcsvlist{⟨manejador⟩}{a,b,c} son las vías rápidas, y \DeclareListParser construye un analizador para el separador que se prefiera. En la práctica el uso más común es recibir una opción de paquete y recorrerla como lista.

pgfkeys: dotar a tu herramienta de una interfaz key=value

pgfkeys es el motor key=value que viaja dentro de PGF/TikZ. La sintaxis familiar de TikZ [draw, thick, fill=blue], y las interfaces al estilo \…setup{...} de muchos paquetes, se apoyan en buena medida en él (en TeX Live 2024, PGF va por la versión 3.1.10, copyright de Till Tantau). En su centro hay una sola orden, \pgfkeys{/my/key=value}. Las claves se reparten en espacios de nombres mediante rutas (familias) separadas por /, y a cada clave se le asigna un manejador que dice qué hacer cuando se la invoca. Definir una clave, en suma, es elegir un manejador.

.store in, .code, .is choice: elegir el manejador adecuado

Tres manejadores cubren la mayor parte del trabajo real: .store in=\macro para guardar el valor tal cual, .code={... #1 ...} para ejecutar código con el valor (que llega como #1), y .is choice para enumerar un conjunto fijo de opciones. Encima de eso, .default=valor aporta el valor que se usa cuando la clave se invoca sin =valor, e .initial=valor da a la clave un valor de partida (legible con \pgfkeysvalueof{/path/key}). Si su paquete expone un punto de entrada del tipo \mypkgsetup{...}, el modismo es \pgfqkeys{/mypkg}{⟨lista de claves⟩}: la «q» es de quick, y es abreviatura de \pgfkeys{/mypkg/.cd, ⟨lista de claves⟩}. Envuelva eso en una macro de una línea y sus usuarios lo configurarán todo con nombres de clave cortos.

document.tex
\usepackage{pgfkeys}

\pgfkeys{
  /book/title/.store in    = \bookTitle,
  /book/edition/.store in  = \bookEd,
  /book/edition/.default   = 1,          % value used when called bare
  /book/pages/.initial     = 100,        % starting value
  /book/layout/.is choice,               % a fixed set of options
  /book/layout/wide/.code   = {\def\bookLayout{WIDE}},
  /book/layout/narrow/.code = {\def\bookLayout{NARROW}},
  /book/note/.code = {\def\bookNote{<<#1>>}},   % #1 is the value passed in
}

\pgfkeys{/book/title=TeX by Topic, /book/edition, /book/layout=wide}
\pgfkeysvalueof{/book/pages}          % -> 100

% a one-line entry point for your users
\newcommand{\mypkgsetup}[1]{\pgfqkeys{/book}{#1}}
\mypkgsetup{title = My Report, edition = 2}

Los mensajes de error de pgfkeys son útilmente concretos, y además buenos términos de búsqueda. Pase una clave jamás definida y obtendrá ! Package pgfkeys Error: I do not know the key '/book/nosuchkey', to which you passed '1', and I am going to ignore it. Perhaps you misspelled it. Pase una elección ausente de una lista .is choice y obtendrá ! Package pgfkeys Error: Choice 'sideways' unknown in choice key '/book/layout'. I am going to ignore this key. Ambos ignoran el problema y siguen adelante: la composición no se detiene, así que una clave mal escrita pasa inadvertida si no se lee el registro. Del lado de LaTeX3 existe el equivalente l3keys (\keys_define:nn y compañía). Un reparto razonable: l3keys para escribir un paquete nuevo en expl3, pgfkeys para encajar con código derivado de TikZ o con una base existente.

Aritmética con reales: \fpeval ya no necesita xfp

La aritmética entera de TeX se resiente en cuanto entran decimales: la división de \numexpr redondea, por ejemplo. Para eso está \fpeval: \fpeval{1/3} da 0.3333333333333333, \fpeval{sqrt(2)} da 1.414213562373095, \fpeval{sind(30)} da 0.5 y \fpeval{round(2/3, 4)} da 0.6667. Para combinarlo con una longitud basta con añadir la unidad: \setlength{\x}{\fpeval{345/7}pt}. Una afirmación que conviene fechar de forma explícita: en el LaTeX2e que trae TeX Live 2024 (la entrega de 2023-11-01), \fpeval, \inteval y \dimeval viven en el núcleo, y \usepackage{xfp} no hace falta. El propio xfp los define ahora con \ProvideExpandableDocumentCommand —«súplelos si faltan»—, de modo que cargarlo no perjudica y sigue siendo la opción segura si además hay que dar soporte a instalaciones antiguas.

Por último, una guía somera para combinar los tres. Para cambiar ligeramente el comportamiento ajeno desde su preámbulo, eche mano de etoolbox y ponga siempre un aviso en la rama de fallo. Para dotar a su propio paquete de una interfaz de configuración, eche mano de pgfkeys o l3keys. Para calcular una dimensión o una proporción, eche mano de \fpeval. Y la primera pregunta es siempre si se puede evitar parchear del todo: pruebe \renewcommand sobre una orden pública, luego un hook como \AtBeginEnvironment, luego una opción de paquete en regla; y solo cuando nada de eso funcione desenfunde \patchcmd. Un parche puede funcionar hoy, pero solo está garantizado hasta la actualización de mañana.