Aides à la programmation

Philipp Lehman est surtout connu pour biblatex et csquotes, mais celle de ses œuvres qui tourne discrètement dans le plus grand nombre de préambules est sans doute la troisième : etoolbox. La raison se ramène presque à une seule commande, \patchcmd, qui ne remplace qu’une partie de la macro d’autrui au lieu de la redéfinir entièrement. Il y a toutefois un piège : si le texte recherché n’est pas trouvé, \patchcmd ne fait absolument rien — ni erreur, ni avertissement. C’est en général ce qui se cache derrière un réglage de préambule qui « cesse de fonctionner » mystérieusement le lendemain d’une mise à jour. Cette page traite des tests, drapeaux, hooks et patches d’etoolbox, puis de pgfkeys, le moteur avec lequel les packages LaTeX bâtissent des interfaces key=value, et enfin de \fpeval pour le calcul sur les réels.

Ce qu’est etoolbox : une boîte à outils e-TeX à visage LaTeX

etoolbox est une boîte à outils de programmation destinée à qui écrit des classes et des packages. Elle réemballe les primitives de bas niveau ajoutées par e-TeX pour qu’elles ressemblent à du LaTeX2e, et y ajoute une belle dose de commodités générales. La version présente dans TeX Live 2024 est la 2.5k, datée du 5 octobre 2020 ; la mention de copyright porte deux noms : Philipp Lehman (2007–2011) et Joseph Wright (2015–2020). Tout moteur TeX moderne embarque e-TeX, donc \usepackage{etoolbox} suffit. Même maintenant qu’expl3, la couche de programmation de LaTeX3, s’est répandue, etoolbox survit parce qu’il s’insère directement dans le monde LaTeX2e : les arguments s’écrivent #1, le branchement est la paire familière {vrai}{faux}, et surtout il y a \patchcmd pour rafistoler après coup le package d’autrui. Pour du vrai travail de préambule, la combinaison est difficile à battre.

Écrire des tests : \ifdef, \ifdefempty, \ifstrequal

Tous les tests d’etoolbox ont la même forme : une paire finale {⟨code si vrai⟩}{⟨code si faux⟩}. Aucun \fi à ne pas oublier, aucune hésitation sur la place du \else. « Cette commande est-elle déjà définie ? » s’écrit \ifdef{\cmd}{vrai}{faux}, ou \ifcsdef{name}{vrai}{faux} si l’on dispose du nom sous forme de chaîne (avec \ifundef et \ifcsundef pour les négations). Côté chaînes : \ifblank pour « n’y a-t-il que des espaces ? », sa négation \notblank, \ifstrequal{chaîne}{chaîne}{vrai}{faux} pour l’égalité de deux chaînes, \ifdefempty{\cmd}{vrai}{faux} pour « le corps de cette macro est-il vide ? » et \ifstrempty{chaîne}{vrai}{faux} pour « la chaîne elle-même est-elle vide ? ». Ne les confondez pas avec \ifdefined, qui est une primitive e-TeX et non un branchement à deux voies d’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}

Il y a ici une distinction que la documentation elle-même rend facile à manquer : \ifstrequal et \ifdefstring ne sont pas expansibles. En consultant la source d’etoolbox, on trouve les deux définis par \newrobustcmd — donc dotés du préfixe \protected d’e-TeX — de sorte qu’ils ne se comportent pas comme prévu dans \edef, \typeout ou \csname. Écrivez \typeout{\ifstrequal{abc}{abc}{SAME}{DIFF}} et le journal affiche non pas SAME mais littéralement \ifstrequal {abc}{abc}{SAME}{DIFF}. \ifdefempty, en revanche, est expansible et ne laisse dans un \edef que le résultat. Tous branchent correctement dans le corps du document ; la différence n’apparaît que dans un \edef — savoir où passe cette ligne évite d’y perdre une journée à traquer un test qui « ne marche pas ».

Drapeaux booléens : \newtoggle ou \newbool ?

Le choix par défaut doit être \newtoggle, et la raison tient à l’espace de noms : un toggle vit dans son propre espace de noms et ne peut donc jamais entrer en collision avec une commande existante. On le déclare par \newtoggle{draft}, on le bascule par \toggletrue{draft} / \togglefalse{draft} (ou \settoggle{draft}{true}), on branche par \iftoggle{draft}{⟨vrai⟩}{⟨faux⟩} et on inverse par \nottoggle. L’autre famille, le bool, offre la même forme — \newbool{draft}, \setbool{draft}{true}, \booltrue, \boolfalse, \ifbool{draft}{⟨vrai⟩}{⟨faux⟩} — mais s’appuie en interne sur la même mécanique que le \newif de LaTeX et consomme donc un nom de commande, \ifdraft. C’est le point décisif : on prend un bool lorsqu’il faut coopérer avec du code existant fondé sur \newif, et un toggle pour tout le reste.

CommandeSignificationRemarque
\newtoggle{f}Déclare le drapeau f, faux au départEspace de noms propre ; ne consomme aucun nom de commande
\settoggle{f}{v}Met f à v (true / false)Équivaut à \toggletrue / \togglefalse
\iftoggle{f}{T}{F}T si vrai, F si fauxTrois arguments ; pas de \fi
\newbool{f}La version bool d’un drapeauMême mécanique que \newif ; réserve un nom de commande
\ifbool{f}{T}{F}La version bool du branchementS’associe au code existant fondé sur \newif

\newrobustcmd et \robustify : une macro qui ne casse pas

\newrobustcmd s’écrit exactement comme \newcommand mais produit une commande robuste. La différence saute aux yeux avec \meaning : une commande créée par \newcommand annonce \long macro:->…, tandis qu’une commande créée par \newrobustcmd annonce \protected\long macro:->…. Autrement dit, elle contourne la danse en deux temps du \protect traditionnel et utilise directement le préfixe \protected d’e-TeX. Voilà pourquoi elle peut figurer dans un argument mobile — un titre, une légende — sans être expansée et brisée en chemin vers le fichier de table des matières. Pour une commande fragile déjà définie par autrui, \robustify{\cmd} durcit sur place la définition existante.

\patchcmd : ne remplacer qu’une partie de la macro d’autrui

\patchcmd repère une chaîne de recherche dans le corps d’une macro déjà définie et ne remplace que celle-là. Il prend cinq arguments : \patchcmd{\cmd}{⟨recherche⟩}{⟨remplacement⟩}{⟨si succès⟩}{⟨si échec⟩}. Si le texte cherché est trouvé, il substitue et exécute le quatrième argument ; sinon il laisse la macro intacte et exécute le cinquième. Seule la première occurrence est remplacée : avec deux \small dans le corps, seul le premier change. Voici un cas réellement utile. L’environnement thebibliography de la classe article s’ouvre par \section*{\refname} ; remplacer ce \section* par \section transforme la bibliographie en une section numérotée qui figure aussi dans la table des matières. Mesuré, le fichier .toc a bien reçu \contentsline {section}{\numberline {2}References}, et le patch a tenu sa promesse.

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}{}

Quand un patch ne fait rien en silence : \tracingpatches et xpatch

Un \patchcmd en échec est totalement muet. Mesuré : donnez-lui un motif qui ne correspond pas et laissez les deux branches vides, la compilation se termine avec zéro erreur et zéro avertissement, sans la moindre trace dans le journal. D’où la règle d’airain — ne laissez jamais la branche d’échec vide ; mettez-y un \PackageWarning. Vous obtenez alors Package mypkg Warning: bibliography patch failed on input line 5. et vous l’apprenez le lendemain de la mise à jour plutôt que des mois plus tard. Pour comprendre pourquoi, placez \tracingpatches dans le préambule : etoolbox.def est chargé et un diagnostic est écrit dans le journal pour chaque patch.

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

Les diagnostics se répartissent en trois espèces. « Le motif recherché n’est pas dans le corps » (-- search pattern not found in replacement text) est le signe classique d’une mise à jour qui a modifié la définition ; examinez la nouvelle avec \show et réécrivez le texte cherché. « La commande est indéfinie » (-- control sequence is undefined or \relax) signifie que l’on patche trop tôt : reportez le patch plus loin, par exemple dans \AtBeginDocument. Le troisième, « ne peut être retokenisée proprement » (-- macro cannot be retokenized cleanly), est un problème de code de catégorie : la macro a été définie sous un régime de catcodes différent de l’actuel, vérifiez donc que vous patchez à l’intérieur de \makeatletter.

Et il existe un échec qui n’apparaît même pas dans le diagnostic : \patchcmd ne fonctionne pas sur une commande à argument optionnel. Interrogez \meaning au sujet d’un \opt défini par \newcommand{\opt}[2][X]{...} et vous obtenez \@protected@testopt \opt \\opt {X}\opt n’est qu’une porte d’entrée qui aiguille, et le vrai corps loge dans une commande distincte nommée \\opt. \patchcmd{\opt}{small}{LARGE} fouille donc la porte d’entrée et échoue. Dans ce cas, utilisez \xpatchcmd du package xpatch, qui étend etoolbox : mesuré, les mêmes arguments ont réussi et la macro interne est devenue \long macro:[#1]#2-><#1|#2|LARGE>. xpatch fournit aussi les commandes correspondantes pour les environnements.

Hooks, ajouts et listes : glisser son code dans celui des autres

Si l’on peut éviter de réécrire le corps d’une macro, mieux vaut l’éviter. etoolbox fournit un généreux jeu de hooks du type « exécuter ce code à tel moment ». Le début et la fin du document relèvent du noyau LaTeX avec \AtBeginDocument et \AtEndDocument, mais etoolbox ajoute \AtEndPreamble (tout à la fin du préambule), \AfterEndDocument (vraiment en dernier) et des hooks autour d’un environnement précis : \AtBeginEnvironment{⟨env⟩}{⟨code⟩}, \AtEndEnvironment, \BeforeBeginEnvironment et \AfterEndEnvironment. Pour ajouter après coup à une macro ou à un hook existant, on dispose de \appto{\cmd}{⟨code⟩} (à la fin) et \preto{\cmd}{⟨code⟩} (au début) ; \gappto en est la variante globale et \eappto expanse d’abord le code ajouté. Pour une macro à arguments, on emploie \apptocmd / \pretocmd, dotées de branches succès et échec — elles aussi se contentent d’exécuter la branche d’échec sur une commande indéfinie, sans erreur : même prudence que pour \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}

Le versant listes est également couvert. \listadd{\mylist}{⟨élément⟩} ajoute à une liste interne, et \forlistloop{⟨gestionnaire⟩}{\mylist} applique à chaque élément un gestionnaire à un argument. Si l’on dispose déjà d’une chaîne séparée par des virgules, \docsvlist{a,b,c} et \forcsvlist{⟨gestionnaire⟩}{a,b,c} sont les voies rapides ; \DeclareListParser construit un analyseur pour un séparateur de son choix. En pratique, l’usage le plus courant consiste à recevoir une option de package et à la parcourir comme une liste.

pgfkeys : doter son outil d’une interface key=value

pgfkeys est le moteur key=value livré à l’intérieur de PGF/TikZ. La syntaxe familière de TikZ [draw, thick, fill=blue], ainsi que les interfaces de style \…setup{...} de nombreux packages, reposent largement dessus (dans TeX Live 2024, PGF est en version 3.1.10, copyright Till Tantau). En son centre trône une seule commande, \pgfkeys{/my/key=value}. Les clés se répartissent en espaces de noms par des chemins (familles) séparés par /, et chaque clé reçoit un gestionnaire qui dit quoi faire lorsqu’elle est appelée. Définir une clé, en somme, c’est choisir un gestionnaire.

.store in, .code, .is choice : choisir le bon gestionnaire

Trois gestionnaires couvrent l’essentiel du travail réel : .store in=\macro pour conserver la valeur telle quelle, .code={... #1 ...} pour exécuter du code avec la valeur (qui arrive en #1), et .is choice pour énumérer un ensemble fixe d’options. Par-dessus, .default=valeur fournit la valeur employée quand la clé est appelée sans =valeur, et .initial=valeur donne à la clé une valeur de départ (lisible par \pgfkeysvalueof{/path/key}). Si votre package expose un point d’entrée du type \mypkgsetup{...}, l’idiome est \pgfqkeys{/mypkg}{⟨liste de clés⟩} — le « q » signifie quick, abrégé de \pgfkeys{/mypkg/.cd, ⟨liste de clés⟩}. Enveloppez cela dans une macro d’une ligne et vos utilisateurs configurent tout avec de courts noms de clés.

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}

Les messages d’erreur de pgfkeys sont utilement précis et constituent de bons termes de recherche. Passez une clé jamais définie et vous obtenez ! 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. Passez un choix absent d’une liste .is choice et vous obtenez ! Package pgfkeys Error: Choice 'sideways' unknown in choice key '/book/layout'. I am going to ignore this key. Tous deux ignorent le problème et poursuivent : la composition ne s’arrête pas, donc une clé mal orthographiée passe inaperçue si l’on ne lit pas le journal. Du côté LaTeX3 existe l’équivalent l3keys (\keys_define:nn et consorts). Répartition raisonnable : l3keys pour écrire un nouveau package en expl3, pgfkeys pour s’accorder à du code issu de TikZ ou à un existant.

Calcul sur les réels : \fpeval n’a plus besoin d’xfp

L’arithmétique entière de TeX montre ses limites dès qu’il y a des décimales — la division de \numexpr arrondit, par exemple. C’est à cela que sert \fpeval : \fpeval{1/3} donne 0.3333333333333333, \fpeval{sqrt(2)} donne 1.414213562373095, \fpeval{sind(30)} donne 0.5 et \fpeval{round(2/3, 4)} donne 0.6667. Pour le combiner à une longueur, il suffit d’ajouter l’unité : \setlength{\x}{\fpeval{345/7}pt}. Une affirmation à dater explicitement : dans le LaTeX2e livré avec TeX Live 2024 (version du 2023-11-01), \fpeval, \inteval et \dimeval figurent dans le noyau, et \usepackage{xfp} n’est pas nécessaire. xfp lui-même les définit désormais par \ProvideExpandableDocumentCommand — « les fournir s’ils manquent » — si bien que le charger ne nuit pas et reste le choix sûr lorsqu’il faut aussi servir des installations plus anciennes.

Pour finir, un repère sur la façon de combiner les trois. Pour modifier légèrement le comportement d’autrui depuis votre préambule, prenez etoolbox — et mettez toujours un avertissement dans la branche d’échec. Pour doter votre propre package d’une interface de configuration, prenez pgfkeys ou l3keys. Pour calculer une dimension ou un rapport, prenez \fpeval. Et la première question est toujours de savoir si l’on peut éviter tout patch : essayez \renewcommand sur une commande publique, puis un hook comme \AtBeginEnvironment, puis une véritable option de package — et ce n’est que si rien ne marche qu’il faut dégainer \patchcmd. Un patch peut fonctionner aujourd’hui, mais il n’est garanti que jusqu’à la mise à jour de demain.