Pourquoi un RegExp avec indicateur global donne-t-il de mauvais résultats?

277

Quel est le problème avec cette expression régulière lorsque j'utilise l'indicateur global et l'indicateur insensible à la casse? La requête est une entrée générée par l'utilisateur. Le résultat devrait être [vrai, vrai].

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));
result.push(re.test('Foo Bar'));
// result will be [true, false]

var reg = /^a$/g;
for(i = 0; i++ < 10;)
   console.log(reg.test("a"));

à propos
la source
54
Bienvenue dans l'un des nombreux pièges de RegExp en JavaScript. Il possède l'une des pires interfaces de traitement des regex que j'ai jamais rencontrées, pleine d'effets secondaires étranges et de mises en garde obscures. La plupart des tâches courantes que vous souhaitez généralement effectuer avec l'expression régulière sont difficiles à définir correctement.
bobince
XRegExp ressemble à une bonne alternative. xregexp.com
environ
Voir aussi la réponse ici: stackoverflow.com/questions/604860/…
Prestaul
Une solution, si vous pouvez vous en tirer, consiste à utiliser le littéral regex directement au lieu de l'enregistrer re.
thdoan

Réponses:

350

L' RegExpobjet garde une trace de l' lastIndexendroit où une correspondance s'est produite, donc lors des correspondances suivantes, il commencera à partir du dernier index utilisé, au lieu de 0. Regardez:

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));

alert(re.lastIndex);

result.push(re.test('Foo Bar'));

Si vous ne souhaitez pas réinitialiser manuellement lastIndexà 0 après chaque test, supprimez simplement l' gindicateur.

Voici l'algorithme que les spécifications dictent (section 15.10.6.2):

RegExp.prototype.exec (chaîne)

Effectue une correspondance d'expression régulière de chaîne par rapport à l'expression régulière et renvoie un objet Array contenant les résultats de la correspondance, ou null si la chaîne ne correspond pas La chaîne ToString (chaîne) est recherchée pour une occurrence du modèle d'expression régulière comme suit:

  1. Soit S la valeur de ToString (chaîne).
  2. Soit length la longueur de S.
  3. Soit lastIndex la valeur de la propriété lastIndex.
  4. Soit i la valeur de ToInteger (lastIndex).
  5. Si la propriété globale est fausse, soit i = 0.
  6. Si I <0 ou I> longueur, définissez lastIndex sur 0 et retournez null.
  7. Appelez [[Match]], en lui donnant les arguments S et i. Si [[Match]] a renvoyé un échec, passez à l'étape 8; sinon, r soit son résultat d'État et passez à l'étape 10.
  8. Soit i = i + 1.
  9. Passez à l'étape 6.
  10. Soit e la valeur endIndex de r.
  11. Si la propriété globale est vraie, définissez lastIndex sur e.
  12. Soit n la longueur du tableau de captures de r. (Il s'agit de la même valeur que NCapturingParens de 15.10.2.1.)
  13. Renvoie un nouveau tableau avec les propriétés suivantes:
    • La propriété index est définie sur la position de la sous-chaîne correspondante dans la chaîne complète S.
    • La propriété d'entrée est définie sur S.
    • La propriété length est définie sur n + 1.
    • La propriété 0 est définie sur la sous-chaîne correspondante (c'est-à-dire la partie de S entre le décalage i inclus et le décalage e exclusif).
    • Pour chaque entier i tel que I> 0 et I ≤ n, définissez la propriété nommée ToString (i) sur le ième élément du tableau de captures de r.
Ionuț G. Stan
la source
83
C'est comme le guide de l'auto-stoppeur pour la conception de l'API Galaxy ici. "Cet écueil dans lequel vous êtes tombé est parfaitement documenté dans la spécification depuis plusieurs années, si vous aviez pris la peine de vérifier"
Retsam
5
Le drapeau collant de Firefox ne fait pas du tout ce que vous impliquez. Au contraire, il agit comme s'il y avait un ^ au début de l'expression régulière, SAUF que ce ^ correspond à la position actuelle de la chaîne (lastIndex) plutôt qu'au début de la chaîne. Vous testez effectivement si l'expression régulière correspond à "ici" au lieu de "n'importe où après lastIndex". Voir le lien que vous avez fourni!
Doin
1
La déclaration liminaire de cette réponse n'est tout simplement pas exacte. Vous avez mis en évidence l'étape 3 de la spécification qui ne dit rien. L'influence réelle de lastIndexest dans les étapes 5, 6 et 11. Votre déclaration d'ouverture n'est vraie que SI LE DRAPEAU MONDIAL EST DÉFINI.
Prestaul
@Prestaul oui, vous avez raison de ne pas mentionner le drapeau mondial. C'était probablement (je ne me souviens pas de ce que je pensais à l'époque) implicite en raison de la façon dont la question est formulée. N'hésitez pas à modifier la réponse ou à la supprimer et à créer un lien vers votre réponse. Aussi, permettez-moi de vous rassurer que vous êtes meilleur que moi. Prendre plaisir!
Ionuț G. Stan
@ IonuțG.Stan, désolé si mon commentaire précédent semblait agressif, ce n'était pas mon intention. Je ne peux pas le modifier à ce stade, mais je n'essayais pas de crier, juste pour attirer l'attention sur le point essentiel de mon commentaire. Ma faute!
Prestaul
72

Vous utilisez un seul RegExpobjet et vous l'exécutez plusieurs fois. À chaque exécution successive, il continue à partir du dernier index de correspondance.

Vous devez "réinitialiser" l'expression régulière pour recommencer depuis le début avant chaque exécution:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));
// result is now [true, true]

Cela dit, il peut être plus lisible de créer un nouvel objet RegExp à chaque fois (la surcharge est minime car le RegExp est de toute façon mis en cache):

result.push((/Foo B/gi).test(stringA));
result.push((/Foo B/gi).test(stringB));
Roatin Marth
la source
1
Ou tout simplement n'utilisez pas le gdrapeau.
melpomene
36

RegExp.prototype.testmet à jour la lastIndexpropriété des expressions régulières afin que chaque test commence là où le dernier s'est arrêté. Je suggère d'utiliser String.prototype.matchcar il ne met pas à jour la lastIndexpropriété:

!!'Foo Bar'.match(re); // -> true
!!'Foo Bar'.match(re); // -> true

Remarque: le !!convertit en booléen, puis inverse le booléen pour qu'il reflète le résultat.

Alternativement, vous pouvez simplement réinitialiser la lastIndexpropriété:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));
James
la source
12

La suppression du gdrapeau global résoudra votre problème.

var re = new RegExp(query, 'gi');

Devrait être

var re = new RegExp(query, 'i');
user2572074
la source
0

Vous devez définir re.lastIndex = 0 car avec le drapeau g, regex garde la trace de la dernière correspondance, donc le test n'ira pas pour tester la même chaîne, pour cela vous devez faire re.lastIndex = 0

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));
re.lastIndex=0;
result.push(re.test('Foo Bar'));

console.log(result)

Ashish
la source
-1

J'avais la fonction:

function parseDevName(name) {
  var re = /^([^-]+)-([^-]+)-([^-]+)$/g;
  var match = re.exec(name);
  return match.slice(1,4);
}

var rv = parseDevName("BR-H-01");
rv = parseDevName("BR-H-01");

Le premier appel fonctionne. Le deuxième appel ne fonctionne pas. L' sliceopération se plaint d'une valeur nulle. Je suppose que c'est à cause du re.lastIndex. C'est étrange car je m'attendrais à ce qu'un nouveau RegExpsoit alloué chaque fois que la fonction est appelée et non partagée entre plusieurs invocations de ma fonction.

Quand je l'ai changé en:

var re = new RegExp('^([^-]+)-([^-]+)-([^-]+)$', 'g');

Ensuite, je ne reçois pas l' lastIndexeffet de maintien. Cela fonctionne comme je m'y attendais.

Chelmite
la source