Licence CC BY-NC-ND

La TeXnique : Les commandes fragiles

Pourquoi il faut les protéger ?

— Je comprends pas pourquoi mon code compile pas, j’ai juste rajouté une note de bas de page dans ma section ?
— Hum, essaie de rajouter un \protect ici, chez moi ça marche. Source : Beaucoup d’endroits.

Voici un petit bout de discussion assez classique. Je crois qu’on a tous l’impression que personne ne comprend cette histoire de commandes fragiles à protéger, mais que tout le monde l’utilise comme un sort de magie noire qui règle tous les problèmes possibles. Un peu comme un certain « authentique mage vaudou » qui répare apparemment les PCs par télépathie et rend votre voisine amoureuse de vous… Dans ce billet, on va tenter d’expliquer ces exploits (ceux de \protect, hein) !

Où l’expansion est nécessaire

Pour bien comprendre ce billet, il faut savoir le système d’expansion de TeX. J’en parle dans ce précédent billet.

C'est dangereux de sortir !

La première chose à expliquer est la notion d'argument mouvant. Il s’agit tout simplement des arguments que LaTeX sauvegarde (dans un fichier auxiliaire par exemple) pour les relire plus tard. Ce sont typiquement les arguments qui se retrouvent plus tard dans la table des matières, la table des figures et autres, donc les arguments des commandes comme \section et \caption. Par exemple, la commande \tableofcontents lit le contenu du fichier .toc pour savoir ce qu’elle doit afficher. Le contenu de ce fichier a été écrit lors des compilations précédentes par les \chapter, \section et autres commandes de sectionnement.

La commande à argument mouvant développe son argument pour le copier dans le fichier auxiliaire. Par exemple, observons le fichier .toc obtenu avec ce code.

\newcommand{\test}{Test}
\tableofcontents
\section{Section de \test}

Le fichier .toc contient \contentsline {section}{\numberline {1}Section de Test}{1}%. En particulier, il contient Test. Ainsi, lors de la prochaine compilation, \tableofcontents lira le contenu du fichier .toc et saura quoi afficher. On va jouer un peu avec ça en créant une erreur, ça nous permettra de comprendre la suite. Pour créer une erreur, on va empêcher le développement de \test grâce à \unexpanded et on va déclarer \test après l’utilisation de \tableofcontents.

\tableofcontents
\newcommand{\test}{Test}
\section{Section de \unexpanded{\unexpanded{\test}}}

La première compilation est OK, mais la seconde donne une erreur undefined control sequence sur la commande \test ! En fait, cette fois c’est \test et pas Test qui apparaît dans le fichier .toc et donc \tableofcontents veut afficher \test qui est inconnue puisque pas encore définie. Ce test nous apprend que ce qui a été écrit dans le fichier auxiliaire peut être du code qui sera potentiellement lu et développé plus tard. Et c’est là que les ennuis commencent.

Si vous êtes attentifs, vous pourriez vouloir me dire que j’ai quand même bien forcé pour obtenir cette erreur. Les ennuis, j’ai bien creusé pour les trouver quoi. Déjà, si l’argument mouvant est développé, à la fin il n’y aura que des éléments primitifs de TeX et cette erreur n’arrivera pas. Et c’est vrai. Mais quelque chose de pire peut arriver, l’argument peut être développé en du code incorrect.

On pourrait se dire que seul du code incorrect peut être développé en code incorrect. Malheureusement, certaines commandes peuvent être développées en du code incorrect dans le contexte de sauvegarde d’argument. Ce sont exactement ces commandes que l’on qualifie de fragiles. Les commandes qui sont développées en du code correct durant la sauvegarde d’argument sont dites robustes. Maintenant que c’est dit, il nous reste à voir pourquoi un mauvais développement peut arriver, et bien sûr comment l’empêcher.

Tomber malade en sortant

Pour expliquer comment un mauvais développement peut arriver, il faut d’abord se rendre compte que TeX ne fonctionne pas juste à base d’expansion. Il y a de l’expansion, mais il y a aussi de l’exécution. L’expansion correspond juste à remplacer un token par sa signification. C’est juste du remplacement.

Par exemple, la commande \def n’est pas développée. Quand TeX arrive devant un \def, il ne le développe pas, il regarde le token qui vient juste après et il change la signification de ce token (la commande qu’on est en train de définir) ; c’est une phase d’exécution. En gros, tout ce qui ne correspond pas à remplacer un token par sa signification correspond à de l’exécution.

Bien sûr, lorsqu’un document est compilé, l’exécution et le développement travaillent en quelque sorte ensemble. TeX croise un token, il le développe et ensuite il l’exécute si c’est possible. Et ainsi de suite.

Le problème, c’est que dans certains contextes appelé expansion-only context, seul l’expansion se fait et pas l’exécution. Et c’est ça qui peut mener à du code incorrect. Vous l’aurez compris, quand TeX écrit un argument mouvant dans un fichier, il est dans un contexte où seul l’expansion se fait. Voyons quelques exemples avec \def pour mieux comprendre cela.

\def\arg{Bonjour}
\newcommand\test{\def\arg{Test}\arg}
\tableofcontents
\section{\test}

La première compilation se passe bien, on a une section dont le titre est « Test ». Mais la seconde compilation nous donne l’erreur « Missing control sequence inserted ». Regardons le contenu du fichier .toc.

\contentsline {section}{\numberline {1}\def Bonjour{Test} Bonjour}{1}% 

On n’a pas \arg, mais Bonjour à la place. Expliquons cela en développant \test dans un contexte sans exécution.

  • On le développe en \def\arg{Test}\arg.
  • On lit le \def, mais il n’y a pas d’exécution.
  • On lit \arg, on le développe en Bonjour.
  • On garde {, pareil pour T, e, s, t et }.
  • On lit de nouveau \arg, on le développe en Bonjour.

Voici comment on se retrouve avec du code incorrect à parti d’un code qui correct. Félicitations, nous avons créé une commande fragile ! Bien sûr, il y en a déjà dans LaTeX (\footnote par exemple).

Protégez-vous !

Maintenant qu’on sait ce qui pose problème, il n’est pas difficile de régler les choses. Si on ne développe pas \test, il n’y aura pas de problème. Le problème, c’est qu’on ne sait pas combien de fois l’argument sera développé (\section pourrait l’envoyer à une autre commande qui la développe et l’envoie à une autre commande, etc.).

La commande \protect règle cela. En gros, voici le fonctionnement de \protect<token>.

  • Si on est en contexte normal, \protect ne fait rien (en fait il se développe en \relax), donc on obtient quelque chose équivalent à <token>.
  • Si on est en contexte sans exécution, \protect est défini comme étant \noexpand\protect\noexpand et \protect<token> se développe donc en \protect<token>. On est revenu au point de départ et la commande est toujours protégé.

Donc il nous suffit d’utiliser \protect devant les commandes fragiles qui sont dans des arguments mouvants.

\def\arg{Bonjour}
\newcommand\test{\def\arg{Test}\arg}
\tableofcontents
\section{\protect\test}

Nous remarquerons que si une commande est fragile, alors les commandes qui l’utilisent (sans la protéger) sont également fragiles. Mais LaTeX nous donne la possibilité de créer des commandes robustes (même si elles utilisent une commande fragile sans la protéger) avec la commande \DeclareRobustCommand. On peut donc écrire ce code.

\def\arg{Bonjour}
\DeclareRobustommand\test{\def\arg{Test}\arg}
\tableofcontents
\section{\protect\test}

Et finalement, notons que certains packages (etoolbox et xparse par exemple) offrent également de quoi déclarer des commandes robustes et même de quoi rendre une commande fragile robuste !

Protéger des éléments autres que des commandes ?

Parfois, un argument peut poser problème même s’il n’est pas fragile. C’est ce qui arrive dans ce sujet. Comme quoi cette histoire de fragilité est vraiment embêtante !


2 commentaires

Oui effectivement, j’aurais pu regarder l’exemple des notes de bas de pages, mais ça demande de regarder le code de la commande. Dans le cas de \footnote, c’est à cause de l’argument optionnel qu’il prend. Pour avoir cet argument optionnel, on a un \@ifnextchar dans son code qui permet de regarder si le caractère qui suit est un [. Mais \@ifnextchar se développe en un truc qui contient des \let et des \def. Et boom, ça casse.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte