Comment tester si une commande est définie ?#

1.  En #

Le programme original, écrit par Donald Knuth, ne définit pas de commandes dédiées à cette tâche. Heureusement, définit deux primitives supplémentaires :

  • \ifdefined

  • \ifcsname cmd name\endcsname

Les deux commandes utilisées dans l’exemple qui suit produisent le même effet :

\ifdefined\foo
  \message{\string\foo\space is defined}%
\else
  \message{no command \string\foo}%
\fi
%
\ifcsname foo\endcsname
  \message{\string\foo\space is defined}%
\else
  \message{no command \string\foo}%
\fi

However, after using the original \@ifundefined{foo}..., the conditionals will detect the command as « existing » (since it has been \let to \relax) ; so it is important not to mix mechanisms for detecting the state of a command.

2.  En #

Quand on programme en on peut directement utiliser \@ifundefined{⟨cmd name⟩}{⟨action1⟩}{⟨action2⟩}, qui exécute ⟨action1⟩ si la commande **n’**est pas définie, et ⟨action2⟩ dans le cas contraire (⟨cmd name⟩ est le nom de la commande tout nu, sans son antislash \).

Si vous utilisez une version de antérieure à 2018, il faut éviter de mélanger du code qui utilise les primitives avec du code qui utilise \@ifundefined (voir ci-dessous pourquoi). Comme cela peut se produire d’une extension à l’autre, vous n’êtes jamais à l’abri d’une erreur…

Notez également que, même après 2018, va toujours renvoyer « vrai si l’on utilise \@ifundefined avec une commande définie comme un alias de \relax.

3.  Un peu d’histoire#

On trouve dans d’anciennes macros écrites en le procédé suivant pour tester l’existence d’une commande ⟨commande⟩ :

\ifx\⟨commande⟩\undefined⟨code à exécuter⟩

(Ceci exécute le code si la commande **n’**existe pas, bien sûr.)

Le fonctionnement de cette commande repose sur le principe que \undefined n’est jamais défini (donc elle est égale à une autre commande non définie). Le problème est qu’il ne s’agit que d’une convention qui peut être ignorée par un autre auteur de macros : il y a donc toujours un risque que cette macro soit définie dans une extension chargée par l’utilisateur… Utiliser \@undefined, comme on peut le voir dans certaines macros ne fait que déplacer le problème.

La macro \@ifundefined, elle, est définie dans le noyau de ce qui permet d’éviter ce problème. Cependant, avant 2018, elle était définie de la manière suivante :

\expandafter \ifx \csname cmd name\endcsname \relax

Elle utilisait la propriété suivante de \csname : si la commande n’existe pas, elle est créée comme alias de \relax. Cette approche présente deux inconvénients :

  • Chaque utilisation de \@ifundefined avec un nom de commande qui n’existe pas crée cette commande, définie comme identique à \relax ; si cette commande n’est pas redéfinie ensuite, elle est conservée inutilement en mémoire par le moteur

  • Si le même nom de commande est testé ensuite avec la primitive \ifdefined (par exemple dans le code d’une autre extension), le résultat sera un faux positif, car cette primitive considère aussi comme définie la commande \relax et ses alias.

Avant que \@ifundefined ne soit redéfinie dans le noyau pour être basée sur la primitive \ifdefined, David Kastrup a proposé la solution suivante :

{\expandafter}\expandafter\ifx \csname cmd name\endcsname\relax ...

La commande testée est créée et définie comme \relax à l’intérieur du groupe dans lequel est inclus le premier \expandafter : elle n’est donc pas conservée en mémoire après l’exécution de \@ifundefined.