Une expression régulière est un petit programme, et comme tout programme elle se corrige plus facilement quand on la regarde tourner. Tapez un motif, collez du texte, et les correspondances s'allument au fil de la frappe. Tout ici utilise le moteur d'expressions régulières de JavaScript — ce qui compte, car les dialectes de regex diffèrent plus qu'on ne le croit.
Certains motifs peuvent bloquer des heures : celui-ci tourne dans un worker
Le moteur regex de JavaScript fait du retour sur trace, et le retour sur trace peut exploser. L'exemple canonique est (a+)+b : face à une chaîne de trente « a » sans « b », le moteur essaie toutes les façons de répartir ces trente caractères entre le quantificateur intérieur et l'extérieur — plus d'un milliard de chemins — avant de conclure qu'il n'y a pas de correspondance. Ajoutez cinq caractères et cela prend trente fois plus longtemps.
Ce n'est pas théorique. Lancez ce motif dans un testeur naïf et l'onglet du navigateur se fige complètement, car une regex en cours d'exécution ne peut pas être interrompue depuis JavaScript. Cet outil évalue donc votre motif dans un Web Worker, sur un fil séparé, avec un budget d'une seconde. Si le motif le dépasse, le worker est tué et on vous dit que le motif est pathologique, au lieu de vous laisser regarder la page mourir. L'onglet reste réactif tout du long.
La leçon dépasse largement cette page : si des motifs fournis par des utilisateurs atteignent un moteur regex sur votre serveur, ce moteur peut être bloqué de la même manière. Cela porte un nom — ReDoS — et cela a mis à terre de vrais services.
Le drapeau g fait que votre regex se souvient
C'est le bug regex de JavaScript qui coûte le plus d'heures de débogage, et il ressemble à de la magie quand il frappe. Un objet regex portant le drapeau g conserve une propriété lastIndex, et .test() comme .exec() la font avancer. Appeler deux fois le même test sur la même chaîne donne donc des réponses différentes.
Essayez dans une console : const re = /a/g ; re.test("a") renvoie true, puis re.test("a") renvoie false, puis true à nouveau. Cela alterne indéfiniment. Le premier appel trouve la correspondance en position 0 et met lastIndex à 1 ; le second cherche à partir de la position 1, ne trouve rien, et remet lastIndex à 0. Rien n'est cassé — la regex se comporte exactement comme spécifié, et c'est la spécification qui surprend.
Les règles pratiques : ne stockez jamais une regex avec le drapeau g dans une constante de module pour la réutiliser avec .test(). Si vous ne voulez qu'un oui/non, retirez le g. S'il vous le faut, construisez la regex à neuf à chaque usage, ou remettez lastIndex à zéro vous-même. Cet outil contourne tout cela en construisant une nouvelle regex à chaque frappe.
JavaScript n'est pas PCRE
Un motif tiré d'une réponse Perl, PHP ou Python fonctionnera souvent ici, et parfois non, de façons faciles à manquer. JavaScript n'a ni groupes atomiques ni quantificateurs possessifs — précisément les deux fonctionnalités que les autres dialectes offrent pour empêcher l'explosion de retour sur trace décrite plus haut. Il n'a pas de récursion : les motifs classiques pour « trouver des parenthèses équilibrées » sont donc tout simplement impossibles.
Le lookbehind existe bien en JavaScript, depuis ES2018, mais il est arrivé tard : Safari ne l'a livré qu'en version 16.4, en mars 2023. Si vous prenez en charge d'anciens appareils iOS, un motif utilisant (?<=...) ne va pas seulement échouer à correspondre — il lèvera une SyntaxError à la construction de la regex, emportant votre script avec lui. C'est un échec bien plus bruyant qu'une correspondance manquante, et cela vaut la peine de le savoir avant d'en livrer un.
Les drapeaux, en bref
g trouve toutes les correspondances au lieu de s'arrêter à la première. i ignore la casse. m change le sens de ^ et $ : ils correspondent aux limites de ligne plutôt qu'au seul début et à la seule fin de la chaîne entière, ce qu'on veut généralement en testant sur du texte multiligne collé. s fait aussi correspondre le point aux sauts de ligne ; sans lui, un point ne franchit pas un saut, ce qui explique un nombre surprenant de questions « pourquoi mon motif s'arrête-t-il en fin de ligne ».
u active la gestion correcte d'Unicode. Sans lui, le moteur travaille en unités de code UTF-16 : un seul emoji compte pour deux caractères et une classe de caractères peut le couper en deux. Si votre texte contient quoi que ce soit au-delà du plan multilingue de base, il vous faut u.
Quand ne pas utiliser une regex
Une regex trouve des motifs dans du texte plat. Elle ne sait pas compter, ne retient aucune imbrication arbitraire, et n'a aucune notion de structure. HTML, JSON et code source s'imbriquent tous, et c'est pourquoi toute tentative de les analyser avec une regex marche sur les exemples et échoue sur les vraies données. Utilisez un analyseur ; tous les langages en ont un.
Les adresses e-mail méritent leur propre avertissement. La grammaire de la RFC 5322 autorise commentaires, chaînes entre guillemets et constructions imbriquées, et les regex qui l'implémentent fidèlement atteignent des milliers de caractères. Pendant ce temps, une adresse valide peut tout de même rebondir, et une adresse « à l'air invalide » être parfaitement délivrable. Vérifiez qu'il y a une @ avec quelque chose de part et d'autre, puis envoyez un e-mail de confirmation — le seul test qui prouve réellement quelque chose.
Questions fréquentes
Mon motif ou mon texte sont-ils envoyés à un serveur ?
Non. Cet outil porte la mention « client » : le motif s'exécute dans un Web Worker à l'intérieur de votre propre navigateur, c'est-à-dire un fil séparé sur votre machine, pas une machine distante. Rien n'est transmis, rien n'est conservé.
Pourquoi ai-je « Ce motif prend trop de temps » ?
Votre motif a dépassé le budget d'une seconde, ce qui en pratique signifie toujours un retour sur trace catastrophique et non un travail réellement volumineux. Cherchez les quantificateurs imbriqués — un groupe se terminant par + ou * et lui-même répété, comme (a+)+ ou (\d*)* — et les alternatives dont les branches peuvent correspondre au même texte. Rendre la partie intérieure plus spécifique fait généralement s'effondrer le temps de minutes à microsecondes.
Mon motif marche ici mais pas dans mon code. Pourquoi ?
Deux suspects habituels. Le premier est lastIndex, décrit plus haut : votre code réutilise probablement un seul objet regex à drapeau g là où cet outil en construit un neuf à chaque fois. Le second est l'échappement. Dans un littéral de chaîne JavaScript, "\d" n'est que "d" — il vous faut "\\d", ou mieux un littéral de regex comme /\d+/ où aucun échappement de chaîne n'intervient. Le champ de motif ici prend le motif brut, sans couche de chaîne entre les deux.
Comment faire correspondre un point littéral, ou un caractère spécial ?
Mettez une barre oblique inverse devant : \. correspond à un point plutôt qu'à « n'importe quel caractère ». Les caractères qui réclament ce traitement sont . * + ? ^ $ { } ( ) | [ ] \ — et à l'intérieur d'une classe de caractères les règles se détendent, si bien que [.] fonctionne aussi et se lit souvent mieux. Si vous échappez une chaîne provenant d'une saisie utilisateur, faites-le par le code plutôt qu'à la main.
Pourquoi $ ne correspond-il pas à la fin de chaque ligne ?
Parce que sans le drapeau m, ^ et $ s'ancrent à la chaîne entière plutôt qu'à chaque ligne. Ajoutez m à vos drapeaux et ils correspondront à chaque limite de ligne. Cela fait constamment trébucher ceux qui testent un motif sur une saisie multiligne collée, car le motif est juste et ce sont les drapeaux qui ne le sont pas.