xparse (\NewDocumentCommand)

L'équipe LaTeX a commencé à travailler sur un analyseur d'arguments général, xparse, à la fin des années 1990 ; sa pièce maîtresse, \NewDocumentCommand, n'a quitté le statut de package expérimental pour entrer dans le noyau LaTeX qu'avec la version du 1er octobre 2020. Cet apprentissage de vingt ans a une raison d'être. \newcommand sait seulement compter les arguments d'une commande, tandis que \NewDocumentCommand décrit la nature de chacun, sous forme d'une chaîne appelée spécification d'arguments (arg-spec). Passer du comptage à la description donne accès aux variantes étoilées, à plusieurs arguments optionnels indépendants et à des arguments encadrés par des délimiteurs choisis librement : autant de syntaxes que \newcommand est incapable d'exprimer. Cette page détaille ce que promet chaque lettre de spécificateur, quand recourir à \IfNoValueTF plutôt qu'à \IfBooleanTF, et quand il vaut mieux ne rien sortir de tout cet arsenal.

Ce que \newcommand ne sait pas exprimer : un seul argument optionnel, en première position

\newcommand ne construit qu'une seule forme : au plus un argument optionnel entre crochets, et uniquement en première position, suivi d'arguments obligatoires. Tout ce qui dépasse ce cadre exigeait, note LaTeX News 32, de descendre jusqu'à la primitive TeX \def et à la programmation de macros bas niveau. D'où ces mécanismes écrits à la main que l'on trouve dans les sources des anciens packages, chargés d'épier le prochain token : \@ifstar pour repérer une étoile, \@ifnextchar pour repérer un caractère donné. Comme leurs noms contiennent un @, il faut les entourer de \makeatletter ; ils se cassent facilement autour des espaces et des imbrications, et personne n'a jamais pris plaisir à les lire.

\NewDocumentCommand remplace tout cet espionnage par une grammaire déclarative. On lui passe une chaîne de lettres au lieu d'un nombre ; l'analyseur lit l'entrée et livre toujours au corps des arguments normalisés sous la forme #1, #2, etc. L'interface que voient les utilisateurs se trouve ainsi séparée du code qui l'implémente. Dans le noyau, cette mécanique porte le nom de module ltcmd, et depuis la version du 1er octobre 2020 \usepackage{xparse} est inutile. Le package xparse figure toujours sur CTAN, mais le README du bundle l3packages qui le distribue s'intitule désormais « Deprecated » et précise que ce matériel est conservé pour faire tourner d'anciens fichiers. Font exception les types d'arguments obsolètes g/G, l et u : en utiliser un donne Invalid argument type "g" in command "\zzz" (requires xparse). Du code neuf n'a à peu près aucune raison d'en vouloir.

Écrire \NewDocumentCommand et la différence entre New, Renew, Provide et Declare

La forme de base prend trois arguments : \NewDocumentCommand{\cmd}{⟨arg-spec⟩}{⟨corps⟩} — le nom de la commande, la spécification d'arguments et le corps, où les arguments arrivent sous la forme #1, #2, etc. Changer le verbe initial change l'attitude face à un nom déjà pris. Viser avec \NewDocumentCommand un nom déjà défini arrête la compilation sur LaTeX cmd Error: Command "\section" already defined. C'est un garde-fou et non une brimade : pour remplacer une définition existante on emploie \RenewDocumentCommand, et pour ne définir que si rien n'existe encore, \ProvideDocumentCommand.

DéclarationComportement face à un nom déjà défini
\NewDocumentCommandErreur bloquante si le nom est pris — le choix par défaut
\RenewDocumentCommandErreur si le nom n’existe pas ; sert à retravailler une commande existante
\ProvideDocumentCommandNe définit que si rien n’existe ; ainsi un package comble un trou de compatibilité
\DeclareDocumentCommandÉcrase sans condition — la documentation officielle recommande d’en user avec parcimonie

Les commandes issues de ces quatre déclarations possèdent une propriété que personne n'a eu à demander : elles sont robustes dès le départ. Appliquer \meaning à l'une d'elles affiche \protected macro:->…, preuve que le mécanisme \protected d'ε-TeX travaille au niveau du moteur. Placée dans un argument mobile — un titre de section, une légende de figure — une telle commande n'a besoin d'aucun \protect. Pourquoi une définition par \newcommand casse précisément à ces endroits, et ce que \DeclareRobustCommand y faisait, relève de la page « Définir des macros ».

Les spécificateurs d'arguments : ce que signifient m, o, O{}, s, t, r, d, e, v et b

Une spécification d'arguments est une chaîne où une lettre décrit un argument, et les spécificateurs se répartissent en deux familles. La famille obligatoire comprend m, r, R, v, b ; la famille optionnelle o, O, d, D, s, t, e, E. Une règle traverse l'ensemble : un type en majuscule permet de nommer une valeur par défaut, tandis que son homologue en minuscule renvoie le marqueur spécial -NoValue-. Lisez o face à O{...}, d face à D, e face à E, r face à R : le motif se vérifie à chaque fois. En interne, précise la documentation, o, d et O ne sont que des raccourcis vers un argument de type D convenablement construit.

SpécificateurSignificationArrivée dans le corps
mArgument obligatoire : groupe entre accolades ou token uniqueUn #1 ordinaire, accolades extérieures retirées
rr⟨d1⟩⟨d2⟩ — obligatoire, encadré par des délimiteurs choisis-NoValue- après erreur si le délimiteur ouvrant manque
RR⟨d1⟩⟨d2⟩{défaut} — comme r, avec une valeur de repli choisieLa valeur par défaut écrite, en cas d’absence
vArgument verbatim lu comme \verb ; le délimiteur ne peut être %, \, #, {, } ni une espaceLes caractères littéraux ; interdit dans l’argument d’une autre commande
bLe corps d’un environnement ; réservé à \NewDocumentEnvironment, et en dernierTout ce qui se trouve entre \begin et \end
oL’argument optionnel [...] standard-NoValue- s’il n’a pas été fourni
OO{défaut}o doté d’une valeur par défautLa valeur par défaut si absent ; une valeur est donc toujours présente
dd⟨d1⟩⟨d2⟩ — optionnel, délimité par des caractères au choix-NoValue- s’il n’a pas été fourni
DD⟨d1⟩⟨d2⟩{défaut}d doté d’une valeur par défautLa valeur par défaut si absent
sDétecte une étoile * en têteSoit \BooleanTrue, soit \BooleanFalse
tt⟨car⟩ — teste la présence d’un caractère donné ; généralisation de sSoit \BooleanTrue, soit \BooleanFalse
ee{⟨tokens⟩} — un jeu d’embellissements comme ^ et _ ; les tokens doivent être distinctsUn argument par token, -NoValue- pour chaque absent
EE{⟨tokens⟩}{⟨défauts⟩}e avec valeurs par défautSi la liste est plus courte, le reste retombe sur -NoValue-

Les types délimités (r, R, d, D) s'accompagnent de contraintes qu'il vaut mieux connaître. D'abord, les caractères de groupement { et } de TeX ne peuvent servir de délimiteurs : écrire r{} vaut un refus, LaTeX cmd Error: Argument delimiter "" invalid in command "\zzz". On choisit plutôt des caractères naturellement appariés : [], (), <>, "". Ensuite, lorsque le délimiteur est un token de caractère, l'analyseur mémorise le code de catégorie qu'il avait au moment de la définition. Transformez < en lettre par la suite, et ce même < cesse d'être reconnu comme délimiteur. Une séquence de contrôle employée comme délimiteur (du genre \x) y échappe, car elle est identifiée par son nom, quelle que soit sa signification du moment.

latex
% t<char> tests for one character; r()...() is a required delimited argument
\NewDocumentCommand{\pt}{t+ r()}{%
  \IfBooleanTF{#1}{\mathbf{(#2)}}{(#2)}%
}
$\pt(1,2)$    % -> (1,2)
$\pt+(3,4)$   % -> (3,4) in bold

% e{^} picks up an optional ^ embellishment wherever it appears
\NewDocumentCommand{\deriv}{e{^} m m}{%
  \frac{\mathrm{d}\IfNoValueF{#1}{^{#1}}#3}{\mathrm{d}#2\IfNoValueF{#1}{^{#1}}}%
}
$\deriv{x}{f}$        % -> df/dx
$\deriv^{2}{x}{f}$    % -> d^2 f / dx^2

Les modificateurs +, !, > et = placés devant un spécificateur

+ rend un argument long, c'est-à-dire capable d'avaler une ligne blanche et donc un changement de paragraphe. C'est ici que se trouve la première mine pour qui vient de \newcommand : le comportement par défaut est inversé. \newcommand rend tous les arguments longs, et l'on écrit la forme étoilée \newcommand* pour les rendre courts. \NewDocumentCommand fait l'inverse : les arguments sont courts par défaut et l'on place + devant chacun de ceux qui doivent être longs. Une commande fraîchement portée à qui l'on confie un texte contenant une ligne blanche répond donc par ! Paragraph ended before \remark was complete. Le réglage argument par argument est précisément l'intérêt : une seule déclaration peut dire que le titre court tient en un paragraphe tandis que le corps peut en compter plusieurs.

Les trois autres se disent vite. ! interdit une espace juste avant un argument optionnel, et ne s'applique qu'à un argument optionnel final — placé en tête, il donne Invalid argument prefix "!" in command "\remark". C'est ce qu'il faut quand les crochets de \foo{x} [x] doivent être lus comme du texte ordinaire. > introduit un processeur d'argument : avec >{\SplitArgument{2}{;}} m, a;b;c est découpé en trois arguments avant même que le corps ne le voie. Le noyau fournit \SplitArgument, \SplitList, \TrimSpaces, \ProcessList et \ReverseBoolean. = est un modificateur plus récent qui force l'interprétation d'un argument optionnel en paires clé-valeur ; il existe pour que des commandes de longue tradition en texte libre — \caption et les commandes de sectionnement — puissent se doter d'une interface clé-valeur sans casser l'ancienne syntaxe.

latex
% + makes ONE argument long; ! on a trailing optional argument forbids a space
\NewDocumentCommand{\remark}{+m !o}{\par\textbf{Note.} #1 (#2)\par}

\remark{first paragraph

  second paragraph}[tag]
\remark{x} [these brackets stay ordinary text]

% > runs a processor before the body sees the argument
\NewDocumentCommand{\triple}{>{\SplitArgument{2}{;}} m}{\tripleaux#1}
\NewDocumentCommand{\tripleaux}{m m m}{(#1/#2/#3)}
\triple{a;b;c}   % -> (a/b/c)

\IfNoValueTF ou \IfBooleanTF, et la différence entre o et O{}

Il existe deux familles de tests, et le spécificateur décide laquelle s'applique. Pour les types qui renvoient -NoValue-o, d, e —, on emploie \IfNoValueTF{#1}{⟨si absent⟩}{⟨si présent⟩} ; pour ceux qui renvoient un booléen — s, t —, c'est \IfBooleanTF{#1}{⟨vrai⟩}{⟨faux⟩}. Le \IfValueTF logiquement inverse existe aussi, et les deux familles disposent de formes à une seule branche : \IfNoValueT, \IfNoValueF, \IfValueT, \IfValueF, \IfBooleanT, \IfBooleanF. La raison d'être de \IfNoValueTF est ce qu'il y a de plus intéressant : un argument optionnel omis n'est vraiment pas la même chose qu'un argument fourni vide. Le mécanisme de valeur par défaut de \newcommand est incapable d'exprimer cette distinction : la valeur par défaut apparaît, tout simplement, et le fait que rien n'ait été écrit n'atteint jamais le corps.

-NoValue- est un garde-fou bien conçu : il est construit de façon à ne pas correspondre au texte littéral -NoValue-, si bien que \IfNoValueTF{-NoValue-} est logiquement faux. La comparaison de chaînes ne peut donc pas en tenir lieu : il faut toujours tester avec \IfNoValueTF. Le piège classique consiste à confondre o et O{}. Avec o, un argument omis vaut réellement -NoValue- et \IfNoValueTF branche correctement ; avec O{}, une valeur est toujours présente — vide en cas d'omission — et \IfNoValueTF tombe systématiquement dans la branche fausse. Quant à l'oubli pur et simple du test, il se remarque en général au moment où la chaîne -NoValue- s'imprime telle quelle dans le PDF.

Que faut-il alors pour savoir si un argument O{} est vide ? C'est précisément là que la recommandation officielle a changé en juin 2022. Le noyau fournit \IfBlankTF (avec \IfBlankT et \IfBlankF), qui renvoie vrai lorsque l'argument est réellement vide ou ne contient que des espaces. Pour les conceptions à deux arguments optionnels consécutifs, la documentation conseille désormais O{} associé à \IfBlankTF plutôt que deux tests séparés, l'un pour la vacuité, l'autre pour -NoValue-. Inutile donc de convoquer \tl_if_blank:nTF d'expl3 ou \ifblank d'etoolbox. Une nuance : \IfBlankTF considère une commande comme \space comme un contenu véritable — elle imprime une espace, mais en tant que token elle a bien une substance.

latex
% s = optional star, o = optional [..], m = mandatory
\NewDocumentCommand{\heading}{s o m}{%
  \IfBooleanTF{#1}
    {\section*{#3}}%                      starred: unnumbered
    {\IfNoValueTF{#2}
       {\section{#3}}%                     no short title given
       {\section[#2]{#3}}}%                short title for the ToC
}

\heading{A Long Introduction}            % numbered section
\heading[Intro]{A Long Introduction}     % short title in the table of contents
\heading*{Preface}                       % unnumbered

% with O{} the value is always there, so test for blankness instead
\NewDocumentCommand{\tagged}{O{} m}{\IfBlankTF{#1}{#2}{[#1] #2}}

Cet exemple de titre recèle une autre propriété que \newcommand ne sait pas imiter : les arguments optionnels créés par \NewDocumentCommand s'imbriquent sans danger. Dans l'exemple de la documentation, \foo[\baz[stuff]]{more stuff} s'analyse correctement, bien qu'un argument optionnel contienne une commande qui en prend elle-même un. Les crochets d'une définition par \newcommand saisissent naïvement tout jusqu'au prochain ] ; la même entrée se trouve donc tronquée au crochet intérieur. Dès qu'il faut placer une commande à argument optionnel dans un autre argument optionnel, la raison de passer à \NewDocumentCommand est suffisante.

\NewDocumentEnvironment et le type b : recevoir le corps de l'environnement comme argument

Les environnements bénéficient de la même mécanique via \NewDocumentEnvironment{⟨env⟩}{⟨arg-spec⟩}{⟨code de début⟩}{⟨code de fin⟩} (avec \Renew…, \Provide… et \Declare…). Les arguments se donnent juste après \begin{⟨env⟩} et restent visibles depuis le code de début comme depuis celui de fin. Un spécificateur apparaît ici qui n'a pas d'équivalent du côté des commandes : b, le corps même de l'environnement. Placez b en fin de spécification et tout ce qui se trouve entre \begin et \end arrive comme un seul argument, prêt à être transformé, composé deux fois ou écarté sous condition.

Trois usages accompagnent b. D'abord, le corps voit par défaut ses espaces de début et de fin supprimés, ce qui dispense de se soucier des blancs en fin de ligne ; écrire !b désactive ce nettoyage. Ensuite, +b s'impose si le corps peut compter plusieurs paragraphes. Enfin — et cela s'oublie facilement — avec b, le code de fin devient superflu, mais le quatrième argument vide doit malgré tout être écrit ; sans lui, \NewDocumentEnvironment se trompe dans le compte des arguments. Les environnements qui utilisent b peuvent s'imbriquer les uns dans les autres. Pour les bases de \newenvironment et les environnements ordinaires qui se passent de b, voir la page « Environnements personnalisés ».

latex
% b grabs the whole body; + allows several paragraphs; the empty 4th
% argument is still required even though there is no end code left to run
\NewDocumentEnvironment{shout}{O{\bfseries} +b}{#1#2}{}

\begin{shout}[\itshape]
  Loud and clear.
\end{shout}

Quand \NewExpandableDocumentCommand s'impose : en tête de cellule de tableau et dans \edef

Que la déclaration standard produise des commandes robustes — qui ne s'expansent pas à la légère — est un avantage presque partout et une gêne en quelques endroits. L'exemple le plus concret est le début d'une cellule de tableau : la mécanique de tabular exige que toute commande enveloppant \multicolumn soit expansible, alors qu'une commande issue de \NewDocumentCommand bloque délibérément l'expansion grâce à une fonctionnalité du moteur. Il en va de même lorsqu'il faut figer le contenu dans \edef ou \write. C'est à cela que sert \NewExpandableDocumentCommand, avec ses variantes \Renew…, \Provide… et \Declare…. La documentation officielle est nette : n'y recourir que si c'est nécessaire, car des restrictions l'accompagnent.

  • S'il y a des arguments, le dernier doit être m, r ou R — l'un des types obligatoires.
  • Le type verbatim v n'est pas disponible, pas plus que les processeurs d'arguments > ni le modificateur clé-valeur =.
  • Impossible de distinguer \foo[ de \foo{[} : dans les deux cas le crochet est lu comme le début d'un argument optionnel, si bien que la détection des optionnels est moins fiable que dans la version standard.
  • En revanche, les types booléens s et t fonctionnent bel et bien. \IfBooleanTF est lui-même expansible, donc la branche se résout comme prévu jusque dans un \edef.
latex
% a command wrapping \multicolumn must be expandable to work in a cell
\NewExpandableDocumentCommand{\wide}{m}{\multicolumn{3}{c}{#1}}

\begin{tabular}{lcr}
  a & b & c \\
  \wide{spans three columns} \\
\end{tabular}

Lequel choisir : \newcommand ou \NewDocumentCommand ?

Pour une abréviation sans argument, ou avec un ou deux arguments obligatoires, \newcommand suffit amplement. Réécrire \newcommand{\R}{\mathbb{R}} avec \NewDocumentCommand n'apporte que des caractères en plus. \newcommand n'a ni vieilli ni été déprécié : il reste un outil LaTeX à part entière aux côtés de l'interface récente. Les signaux du basculement sont, eux, très nets : quand il faut une variante étoilée, quand un deuxième argument optionnel devient nécessaire, quand la syntaxe d'entrée doit être autre chose que [...], et quand une commande à argument optionnel doit prendre place dans un autre argument optionnel. Dès que l'un de ces cas se présente, une ligne de spécification d'arguments est plus courte et infiniment plus lisible qu'une tuyauterie \@ifstar écrite à la main.

Une dernière ligne directrice pointe dans l'autre sens. \NewDocumentCommand est un outil pour concevoir une syntaxe d'entrée, non un langage pour écrire ce que fait le corps. Dès que l'on découpe des chaînes, empile des conditionnelles ou boucle une fois les arguments récupérés, on est passé sur le terrain d'expl3, la couche de programmation LaTeX3 — celle-là même dans laquelle ltcmd est écrit. Inversement, pour concevoir les commandes destinées aux utilisateurs d'un package ou d'une classe, \NewDocumentCommand s'impose : la spécification d'arguments se lit du même coup comme le cahier des charges de l'interface.