3 points par GN⁺ 2024-03-21 | 1 commentaires | Partager sur WhatsApp
  • En Python, avec re, $ peut correspondre non seulement à la fin de chaîne, mais aussi juste avant le dernier saut de ligne final, même lorsque le mode multiligne est désactivé
  • Parce que ^ ressemble à « début de chaîne », il ne faut pas supposer que $ se comporte de manière parfaitement symétrique ; sa signification réelle varie selon l’implémentation de l’expression régulière
  • Pour "cat\n", les résultats de $, \z et \Z diffèrent entre PHP, ECMAScript, Python, Go, Java 8, .NET 7.0 et Rust, et \z a été ajouté en Python seulement dans Python 3.14
  • Si l’on autorise le saut de ligne final, alors $ en mode multiligne correspond à "cat\n" sur toutes les plateformes du tableau ; en revanche, pour ne viser que la vraie fin hors saut de ligne, le choix de la syntaxe change
  • Si l’on ne veut pas faire correspondre le dernier saut de ligne, il faut utiliser \z sur la plupart des plateformes, et envisager d’autres alternatives pour Python avant la 3.14 et pour ECMAScript

$ correspond dans re de Python

  • Dans le module d’expressions régulières re de Python, $ peut correspondre à la fin de chaîne ou juste avant le dernier saut de ligne final, même lorsque le mode multiligne est désactivé
  • cat$ correspond bien à "lolcat" et pas à "internet cat video", ce qui semble simple, mais avec une fin de chaîne comme "cat\n", le résultat peut être différent de ce que l’on attend
  • Quand re.MULTILINE est activé, $ correspond à la fin de la chaîne ainsi qu’à la fin de chaque ligne, c’est-à-dire juste avant chaque saut de ligne
  • Même avec le comportement par défaut, $ correspond à la fin de la chaîne et, s’il y a un saut de ligne final, juste avant ce saut de ligne

Correspondre à la fin en excluant le dernier saut de ligne

  • Pour viser strictement la fin de la chaîne, $ seul peut ne pas suffire, et \z comme \Z sont des candidats possibles comme ancres de fin
  • D’après la documentation des expressions régulières Python et cette explication d’autres syntaxes regex, la prise en charge et la signification de \z et \Z varient selon l’implémentation
  • Pour "cat\n", les différences sont les suivantes
    • PHP : "cat$" correspond avec ou sans mode multiligne, "cat\z" ne correspond pas, et "cat\Z" correspond
    • ECMAScript : "cat$" en mode multiligne correspond, "cat$" hors mode multiligne ne correspond pas, et \z comme \Z ne sont pas pris en charge
    • Python : "cat$" correspond avec ou sans mode multiligne, et "cat\z" comme "cat\Z" ne correspondent pas à "cat\n"
    • Go et Rust : "cat$" en mode multiligne correspond, "cat$" hors mode multiligne et "cat\z" ne correspondent pas, et \Z n’est pas pris en charge
    • Java 8 et .NET 7.0 : "cat$" correspond avec ou sans mode multiligne, "cat\z" ne correspond pas, et "cat\Z" correspond
  • \z en Python a été ajouté dans Python 3.14 ; il n’était pas pris en charge dans les versions précédentes
  • Si l’on autorise le saut de ligne final, alors $ en mode multiligne correspond de façon cohérente à "cat\n" sur toutes les plateformes du tableau
  • Si l’on ne veut pas faire correspondre le saut de ligne final, il faut utiliser \z sur la plupart des plateformes ; avant Python 3.14, il faut utiliser \Z, et en ECMAScript, $ sans mode multiligne
  • Les données du tableau ont été collectées sur regex101.com et n’ont pas été vérifiées dans de vrais environnements d’exécution

1 commentaires

 
GN⁺ 2024-03-21
Avis de Hacker News
  • Depuis longtemps, je considère ^ comme le « début de ligne » et $ comme la « fin de ligne ».
    Quand on manipule des expressions régulières, on traite souvent le texte ligne par ligne, donc le résultat est souvent le même, mais la façon dont je me représente ces opérateurs reste plus proche de la « ligne » que de la « chaîne ».
    C’est sans doute parce que j’ai découvert les expressions régulières avec grep, ce qui m’a donné l’habitude de voir l’entrée comme des lignes plutôt que comme une chaîne.

    • Moi aussi, en voyant le titre, je me suis dit : « Évidemment que non, où ont-ils entendu ça ? »
      J’utilise les expressions régulières depuis près de 20 ans, mais je crois que c’est la première fois que j’entends dire que $ signifie la fin de la chaîne ; je l’ai toujours vu comme la fin de ligne.
    • Ça me gêne que l’article décrive ^ comme le « début de la chaîne ».
      En réalité, tout comme $ est la « fin de ligne », ^ est aussi le « début de ligne », et le début de la chaîne ressemble plutôt à \A, la fin de la chaîne à \Z.
    • Je pensais pareil, mais après avoir essayé directement en Perl, $ se comporte par défaut comme une assertion de lookahead positive sur la fin de la chaîne.
      Il ne matche ni ne consomme le caractère de saut de ligne.
      En mode multiligne seulement, il matche les positions de saut de ligne, mais même là, il semble ne pas les consommer.
      En pratique, en utilisant $, je n’ai pas pu créer une regexp qui capture le dernier caractère d’une ligne, consomme le saut de ligne, puis capture le premier caractère de la ligne suivante ; le groupe de capture s’arrête simplement à $.
    • C’est Vim, plus que grep, qui m’a inculqué cette perception.
  • Les expressions régulières POSIX et celles de Python sont différentes.
    En général, la syntaxe des expressions régulières n’est pas universelle : il faut donc consulter la documentation de l’implémentation utilisée.
    Selon le chapitre 9 de POSIX, les expressions régulières opèrent sur des chaînes, mais certains utilitaires limitent le traitement ligne par ligne.
    Il y est aussi indiqué que $ est une ancre fixée à la fin de la chaîne à faire matcher ; au final, c’est donc l’utilitaire ou le mode qui détermine si $ signifie la fin de chaîne ou la fin de ligne.
    Les outils courants comme grep, sed, awk ou Python fonctionnent par défaut ligne par ligne, et le traitent donc généralement comme une fin de ligne.
    Il n’existe pas une seule syntaxe universelle d’expressions régulières.
    Sans savoir quel langage et quelles options sont utilisés, on ne peut pas lire ni écrire une expression régulière de manière fiable.
    https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...

  • Ce sujet est une très bonne occasion de faire découvrir Robert Elder à ceux qui ne le connaissent pas.
    Il produit du bon contenu sur YouTube et sur son blog, et dans sa série sur les expressions régulières, il creuse assez profondément les différences de comportement entre les implémentations des divers outils.
    Sa vidéo récente est bonne aussi : https://www.youtube.com/watch?v=ys7yUyyQA-Y
    Il a aussi beaucoup de contenus susceptibles d’intéresser les lecteurs de HN, et aborde des sujets comme la réalité et les difficultés du consulting.
    https://www.youtube.com/@RobertElderSoftware
    https://blog.robertelder.org/
    https://blog.robertelder.org/regular-expressions/
    https://www.youtube.com/watch?v=cK87ktENPrI

  • Quand j’ai appris Perl, les expressions régulières ont été l’une des premières choses que j’ai vraiment intégrées, et aujourd’hui encore, grâce au livre « Camel », Perl garde une place confortable dans un coin de ma tête.
    Le savoir le plus important aujourd’hui, c’est que ça dépend de l’implémentation ; j’ai donc pris l’habitude de sortir la fiche de référence correspondante à chaque fois que je travaille sur quelque chose.
    Par exemple, les expressions régulières d’Emacs ne prennent pas en charge les caractères de mot sous la forme \w et il faut utiliser des classes de caractères du genre \s_-, ce qui est agaçant, mais je trouve qu’Emacs est excellent pour la documentation et la découvrabilité.
    Certains utilitaires exigent d’échapper les parenthèses, d’autres non ; parfois ce comportement est configurable, parfois non.
    Après être passé par toutes les phases de confusion, d’agacement et de déni, je l’accepte simplement.
    Les concepts sont les mêmes partout, mais les dialectes changent.

    • Mon cerveau pense en expressions régulières Perl, puis je traduis vers les incohérences du langage que j’utilise.
      Dans le shell en particulier, plutôt que de me demander si sed/grep/awk sont GNU ou BSD, j’insère bien plus souvent perl dans le pipeline.
    • Je suis curieux de savoir comment tu as réussi à les intégrer.
      Perl donne l’impression qu’un chat a marché sur le clavier.
  • J’ai l’impression d’entendre d’innombrables mauvais responsables du recrutement ajouter « comment matchez-vous la fin d’une chaîne dans une expression régulière ? » à leur liste de questions pièges.

  • C’est étrange d’omettre Perl d’une liste sur les expressions régulières.
    Dans la documentation perlre, $ est décrit ainsi : il matche la fin de la chaîne, ou l’emplacement avant le saut de ligne final de la chaîne, ou, avec /m, avant n’importe quel saut de ligne.

    • Omettre Perl, qu’on peut considérer comme le langage le plus fortement associé aux expressions régulières, semble être un oubli assez important.
      Cela montre peut-être aussi à quel point Perl est aujourd’hui sorti du champ d’attention.
  • Raku, anciennement Perl 6, a défini ^ et $ comme le début/la fin de chaîne, et introduit ^^ et $$ pour le début/la fin de ligne
    Il n’y a pas de mode multiligne, et il n’est pas nécessaire
    \h représente aussi les espaces horizontaux, \v les espaces verticaux
    C’est l’avantage d’avoir tout repensé et réécrit : on a pu apprendre du fait que l’ancien comportement surprenait les gens

    • C’est pour ça que, moi qui suis têtu, je n’arrive pas à utiliser Perl 6
      J’ai l’impression qu’on a mélangé au hasard une syntaxe façon bruit de ligne apprise pendant des décennies
      L’inverse aurait été plus clair comme valeur par défaut
      Il aurait semblé plus naturel d’utiliser ^ et $ pour les lignes, et ^^ et $$ pour les chaînes
      Parce que cela ressemble à ^^line1$\n^line2$\n^line3$\n$
      En plus, Perl 6 n’est pas partout, alors que Perl 5 est partout
    • Moi, j’aurais probablement choisi exactement l’inverse
      ^^ paraît plus « début » que ^
    • Presque toutes les regex que j’ai écrites supposaient le début/la fin de chaîne
      Comme on traite généralement des lignes en les passant à une regex, le choix d’utiliser un seul ^ et $ pour toute la chaîne préserve dans une certaine mesure la rétrocompatibilité
  • Je me demande si quelqu’un considère vraiment que les expressions régulières sont standardisées
    À chaque passage à un nouvel environnement, j’ai toujours dû les réapprendre

    • À un moment, j’ai eu l’impression de connaître tous les dialectes
      Il existe sûrement d’autres dialectes de regex, mais je ne les rencontre pas, et ce que je connais suffit dans la plupart des cas
      C’est un peu comme conduire une voiture de location
      Elle se comporte un peu différemment de ma voiture, avec des fonctions manquantes et d’autres en plus, mais dans l’ensemble elles se ressemblent presque toutes
    • La bibliothèque standard ISO/IEC 14882 C++ exige l’implémentation de six syntaxes d’expressions régulières constituant de fait des standards officiels : IEEE Std 1003.1-2008, c’est-à-dire les BRE, ERE, awk, grep et egrep de POSIX, ainsi qu’ECMA-262 EcmaScript 3
      Donc, au moins pour moi, les regex sont standardisées par plusieurs standards officiels publiés
      https://open-std.org/jtc1/sc22/…
      https://pubs.opengroup.org/onlinepubs/9699919799/…
      https://262.ecma-international.org/14.0/…
    • Les grandes familles que je connais sont POSIX, Perl/PCRE, et à peu près RE2, utilisé du côté de Go
      Beaucoup de systèmes, y compris JavaScript, ont implémenté PCRE, parce que Perl ajoutait beaucoup d’extensions utiles au système POSIX
      Si je me souviens bien, RE2 cherchait à limiter les problèmes de performances et les comportements étranges des systèmes existants, et je pensais qu’il avait été entièrement implémenté en Go
      J’ai découvert plus tard que RE2 était antérieur à Go
    • Les langages apparus après Perl utilisent généralement une variante de la syntaxe des regex de Perl, mais il y a toujours de petites différences
      Malgré tout, le sens de $ et la manière de passer en mode multiligne sont en général assez cohérents
    • Fait intéressant, la RFC 9485 https://datatracker.ietf.org/doc/rfc9485/ « I-Regexp: An Interoperable Regular Expression Format » vient tout juste d’être publiée en octobre dernier
  • Les gens confondent chaînes et lignes
    Une chaîne est une séquence de caractères, tandis qu’une ligne peut être vue de deux façons
    Si l’on considère le saut de ligne comme un terminateur de ligne, une ligne est constituée de zéro ou plusieurs caractères autres qu’un saut de ligne, suivis d’un saut de ligne ; sans saut de ligne final, ce n’est pas une ligne complète
    C’est le point de vue adopté par POSIX
    Si l’on considère le saut de ligne comme un séparateur de lignes, une ligne est une séquence de zéro ou plusieurs caractères autres qu’un saut de ligne
    Dans les deux cas, le contenu de la ligne se termine avant le saut de ligne
    La sémantique de ^ et $ est orientée ligne, que l’on soit en mode monoligne ou multiligne
    Pour une sémantique orientée chaîne — ou, lorsqu’on manipule des fichiers, parfois orientée fichier entier — il faut utiliser \A et \Z, ou leurs équivalents
    Les deux interprétations ont leurs avantages
    Quand on transmet du texte sur une liaison série, utiliser le saut de ligne comme terminateur permet de savoir facilement si l’on a reçu une ligne complète
    Dans un fichier texte, voir le saut de ligne comme un séparateur peut être plus pratique, car la dernière ligne ne se retrouve pas dans un état invalide ; mais avec un terminateur de ligne, on peut détecter une ligne écrite de manière incomplète

  • Cela a provoqué plusieurs bugs graves dans des applications basées sur Ruby
    Il faut toujours utiliser \A\z
    https://homakov.blogspot.com/2012/05/saferweb-injects-in-var...
    https://sakurity.com/blog/2015/02/28/openuri.html
    https://sakurity.com/blog/2015/06/04/mongo_ruby_regexp.html