3 Punkte von GN⁺ 2024-03-21 | 1 Kommentare | Auf WhatsApp teilen
  • In Python re kann $ auch dann nicht nur auf das String-Ende, sondern auch vor dem letzten Zeilenumbruch am Ende matchen, wenn der Multiline-Modus deaktiviert ist
  • Nur weil ^ wie „String-Anfang“ wirkt, sollte man nicht annehmen, dass $ vollkommen symmetrisch dazu funktioniert; die tatsächliche Bedeutung hängt von der jeweiligen Regex-Implementierung ab
  • Die Ergebnisse für $, \z und \Z bei "cat\n" unterscheiden sich zwischen PHP, ECMAScript, Python, Go, Java 8, .NET 7.0 und Rust; \z wurde in Python erst in Python 3.14 neu hinzugefügt
  • Wenn ein Zeilenumbruch am Ende erlaubt ist, matcht $ im Multiline-Modus in allen Plattformen der Tabelle auf "cat\n"; soll aber nur das Ende ohne Zeilenumbruch gematcht werden, muss die Syntax je nach Plattform anders gewählt werden
  • Wenn der letzte Zeilenumbruch nicht mitgematcht werden darf, verwendet man auf den meisten Plattformen \z; für Python vor 3.14 und für ECMAScript muss man jeweils andere Alternativen wählen

Positionen, auf die $ in Python re matcht

  • Im Python-RegEx-Modul re kann $ auch dann auf das String-Ende oder direkt vor den letzten Zeilenumbruch am Ende des Strings matchen, wenn der Multiline-Modus deaktiviert ist
  • cat$ matcht auf "lolcat", aber nicht auf "internet cat video", was zunächst simpel wirkt; bei einem abschließenden Zeilenumbruch wie in "cat\n" kann das Ergebnis jedoch anders ausfallen als erwartet
  • Wenn re.MULTILINE gesetzt ist, matcht $ auf das String-Ende und auf jedes Zeilenende, also direkt vor jedem Zeilenumbruch
  • Auch in der Standardeinstellung matcht $ auf das String-Ende; endet der String mit einem Zeilenumbruch, matcht es zusätzlich direkt davor

Matchen ohne den letzten Zeilenumbruch

  • Wenn wirklich nur das String-Ende strikt gematcht werden soll, reicht $ allein möglicherweise nicht aus; \z und \Z kommen als mögliche Endanker infrage
  • Laut der Python-Dokumentation zu regulären Ausdrücken und dieser Erklärung anderer Regex-Syntaxen unterscheiden sich Unterstützung und Bedeutung von \z und \Z je nach Implementierung
  • Die Unterschiede bei "cat\n" sehen so aus:
    • PHP: "cat$" matcht unabhängig vom Multiline-Modus, "cat\z" matcht nicht und "cat\Z" matcht
    • ECMAScript: Multiline-"cat$" matcht, nicht-Multiline-"cat$" matcht nicht, \z und \Z werden nicht unterstützt
    • Python: "cat$" matcht unabhängig vom Multiline-Modus, "cat\z" und "cat\Z" matchen nicht auf "cat\n"
    • Go und Rust: Multiline-"cat$" matcht, nicht-Multiline-"cat$" und "cat\z" matchen nicht, \Z wird nicht unterstützt
    • Java 8 und .NET 7.0: "cat$" matcht unabhängig vom Multiline-Modus, "cat\z" matcht nicht und "cat\Z" matcht
  • \z wurde in Python neu in Python 3.14 hinzugefügt; in früheren Versionen wurde es nicht unterstützt
  • Wenn ein Zeilenumbruch am Ende erlaubt ist, matcht $ im Multiline-Modus in allen Plattformen der Tabelle konsistent auf "cat\n"
  • Soll der Zeilenumbruch am Ende nicht mitgematcht werden, sollte man auf den meisten Plattformen \z verwenden; in Python vor 3.14 stattdessen \Z, in ECMAScript dagegen nicht-Multiline-$
  • Die Daten der Tabelle wurden auf regex101.com gesammelt und nicht in echten Laufzeitumgebungen getestet

1 Kommentare

 
GN⁺ 2024-03-21
Hacker-News-Kommentare
  • Ich habe ^ schon immer als „Zeilenanfang“ und $ als „Zeilenende“ verstanden.
    Beim Arbeiten mit regulären Ausdrücken verarbeitet man Text oft zeilenweise, daher ist das Ergebnis häufig dasselbe, aber die Art, wie ich mir diesen Operator vorstelle, ist weiterhin näher an „Zeile“ als an „String“.
    Vermutlich hat es viel damit zu tun, dass ich reguläre Ausdrücke über grep kennengelernt habe und mir deshalb angewöhnt habe, Eingaben als Zeilen statt als Strings zu sehen.

    • Als ich den Titel sah, dachte ich auch: „Natürlich nicht, wo hat man denn so etwas gehört?“
      Ich benutze seit fast 20 Jahren reguläre Ausdrücke, aber ich glaube, ich höre zum ersten Mal, dass $ das Ende eines Strings sei; ich habe es immer als Zeilenende betrachtet.
    • Mich stört, dass der Artikel ^ als „Anfang des Strings“ bezeichnet.
      Tatsächlich ist ^, so wie $ „Zeilenende“ ist, der „Zeilenanfang“; der Anfang eines Strings scheint eher \A zu sein und das Ende eines Strings eher \Z.
    • Das dachte ich auch, aber als ich es direkt in Perl ausprobiert habe, verhält sich $ standardmäßig wie eine positive Lookahead-Assertion auf das Stringende.
      Es matcht und konsumiert das Zeilenumbruchzeichen nicht.
      Nur im Mehrzeilenmodus matcht es an Zeilenumbruchpositionen, aber auch dann scheint es nicht zu konsumieren.
      Tatsächlich konnte ich mit $ keinen regulären Ausdruck bauen, der das letzte Zeichen einer Zeile erfasst, den Zeilenumbruch konsumiert und dann das erste Zeichen der nächsten Zeile erfasst; die Capture Group endet einfach bei $.
    • Bei mir hat weniger grep als vielmehr Vim diese Vorstellung geprägt.
  • POSIX-reguläre Ausdrücke und Python-reguläre Ausdrücke sind verschieden.
    Im Allgemeinen ist die Syntax regulärer Ausdrücke nicht universell, daher muss man die Dokumentation der verwendeten Implementierung lesen.
    Laut POSIX Kapitel 9 arbeiten reguläre Ausdrücke auf Strings, aber einige Utilities beschränken die Verarbeitung auf Zeilen.
    Außerdem wird $ als Anchor beschrieben, der an das Ende des zu matchenden Strings gebunden ist; ob $ am Ende also das Stringende oder das Zeilenende bedeutet, entscheidet das jeweilige Utility oder der Modus.
    Gängige Tools wie grep, sed, awk und Python arbeiten standardmäßig zeilenweise und behandeln es daher normalerweise als Zeilenende.
    Es gibt nicht die eine universelle Regex-Syntax.
    Ohne zu wissen, welche Sprache und welche Optionen verwendet werden, kann man reguläre Ausdrücke nicht zuverlässig lesen oder schreiben.
    https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...

  • Bei diesem Thema ist es eine gute Gelegenheit, Robert Elder Leuten vorzustellen, die ihn noch nicht kennen.
    Er macht gute Inhalte auf YouTube und in seinem Blog, und in seiner Regex-Serie geht er ziemlich tief auf die Unterschiede im Regex-Verhalten ein, wie es von verschiedenen Tools implementiert wird.
    Auch sein jüngstes Video ist gut: https://www.youtube.com/watch?v=ys7yUyyQA-Y
    Es gibt dort viele Inhalte, die HN-Leser interessieren dürften, darunter auch Themen wie die Realität und die Mühen des Consultings.
    https://www.youtube.com/@RobertElderSoftware
    https://blog.robertelder.org/
    https://blog.robertelder.org/regular-expressions/
    https://www.youtube.com/watch?v=cK87ktENPrI

  • Als ich Perl lernte, gehörten reguläre Ausdrücke zu den ersten Dingen, die mir wirklich in Fleisch und Blut übergingen, und Perl hat bis heute dank des „Camel“-Buchs einen gemütlichen Platz in einer Ecke meines Herzens.
    Das heute wichtigste Wissen ist, dass es je nach Implementierung anders ist, daher habe ich mir angewöhnt, bei jeder Aufgabe die jeweilige Referenztabelle hervorzuholen.
    Emacs-Regexps unterstützen zum Beispiel keine Wortzeichen in der Form \w; man muss eine Zeichenklasse wie \s_- verwenden, was nervig ist, aber bei Dokumentation und Auffindbarkeit halte ich Emacs für erstklassig.
    Bei manchen Utilities müssen Klammern escaped werden, bei anderen nicht; manchmal ist dieses Verhalten konfigurierbar, manchmal nicht.
    Ich habe alle Phasen von Verwirrung, Ärger und Verleugnung durchlaufen und akzeptiere es jetzt einfach.
    Die Konzepte sind überall gleich, aber die Dialekte unterscheiden sich.

    • Mein Kopf denkt in Perl-regulären Ausdrücken und übersetzt das dann in die Inkonsistenzen der Sprache, die ich gerade benutze.
      Besonders in der Shell füge ich viel häufiger perl in eine Pipeline ein, als mir auch noch zu überlegen, ob sed/grep/awk nun GNU oder BSD sind.
    • Mich würde interessieren, wie du das verinnerlicht hast.
      Perl sieht aus, als wäre eine Katze über die Tastatur gelaufen.
  • Ich höre förmlich, wie unzählige schlechte Hiring Manager „Wie matcht man in einem regulären Ausdruck das Ende eines Strings?“ in ihre Liste der Fangfragen aufnehmen.

  • Es ist seltsam, Perl aus einer Liste zu regulären Ausdrücken herauszulassen.
    In der perlre-Dokumentation wird $ so beschrieben: Es matcht das Ende des Strings, oder vor einem Zeilenumbruch am Ende des Strings, oder mit /m vor einem beliebigen Zeilenumbruch.

    • Perl wegzulassen, die Sprache, die man wohl am stärksten mit regulären Ausdrücken verbindet, wirkt wie eine ziemlich große Auslassung.
      Vielleicht zeigt das auch, wie sehr Perl heutzutage aus dem Blickfeld geraten ist.
  • Raku, früher Perl 6, hat ^ und $ als Anfang/Ende der Zeichenkette festgelegt und ^^ sowie $$ für Zeilenanfang/-ende eingeführt.
    Einen Mehrzeilenmodus gibt es nicht und braucht es auch nicht.
    \h steht außerdem für horizontalen Whitespace, \v für vertikalen Whitespace.
    Weil alles vollständig neu gedacht und neu geschrieben wurde, konnte man daraus lernen, dass das alte Verhalten die Leute überrascht hat.

    • Deshalb kann dieser Starrkopf Perl 6 nicht benutzen.
      Es fühlt sich an wie eine zufällige Mischung aus line-noise-artiger Syntax, die man sich über Jahrzehnte angeeignet hat.
      Umgekehrt wäre es als Default klarer gewesen.
      ^ und $ für Zeilen, ^^ und $$ für Zeichenketten zu verwenden, hätte natürlicher gewirkt.
      Weil es wie ^^line1$\n^line2$\n^line3$\n$ aussieht.
      Außerdem ist Perl 6 nicht überall, Perl 5 dagegen schon.
    • Ich hätte mich vermutlich genau umgekehrt entschieden.
      ^^ sieht noch mehr nach „Anfang“ aus als ^.
    • Fast alle regulären Ausdrücke, die ich geschrieben habe, gingen von Anfang/Ende der Zeichenkette aus.
      Weil man normalerweise Zeilen an reguläre Ausdrücke übergibt und verarbeitet, erhält die Entscheidung, ein einzelnes ^ und $ für die gesamte Zeichenkette zu verwenden, ein gewisses Maß an Abwärtskompatibilität.
  • Ich frage mich, ob irgendjemand reguläre Ausdrücke wirklich als standardisiert betrachtet.
    Bei jeder neuen Umgebung musste ich sie immer wieder neu lernen.

    • Irgendwann hatte ich das Gefühl, alle Dialekte zu kennen.
      Es gibt sicher noch mehr Regex-Dialekte, aber ich begegne ihnen nicht, und mit dem, was ich kenne, lässt sich das meiste lösen.
      Es ist ähnlich wie ein Mietwagen.
      Er verhält sich etwas anders als mein Auto, es fehlen Funktionen und es gibt zusätzliche, aber im Großen und Ganzen sind die meisten ziemlich ähnlich.
    • Die ISO/IEC-14882-C++-Standardbibliothek verlangt Implementierungen von sechs de-facto-gesetzlichen Standard-Syntaxen für reguläre Ausdrücke: IEEE Std 1003.1-2008, also POSIX BRE, ERE, awk, grep, egrep sowie ECMA-262 EcmaScript 3.
      Daher sind reguläre Ausdrücke zumindest aus meiner Sicht durch mehrere veröffentlichte offizielle Standards standardisiert.
      https://open-std.org/jtc1/sc22/…
      https://pubs.opengroup.org/onlinepubs/9699919799/…
      https://262.ecma-international.org/14.0/…
    • Die großen Zweige, die ich kenne, sind etwa POSIX, Perl/PCRE und RE2, das im Go-Umfeld verwendet wird.
      Viele Systeme, darunter JavaScript, haben PCRE implementiert, weil Perl dem POSIX-System viele nützliche Erweiterungen hinzugefügt hat.
      Soweit ich mich erinnere, zielte RE2 darauf ab, Performance-Probleme und Sonderverhalten bestehender Systeme einzudämmen, und ich dachte, es sei vollständig in Go implementiert.
      Später stellte ich fest, dass ich nicht wusste, dass RE2 älter als Go ist.
    • Sprachen, die nach Perl kamen, verwenden meist irgendeine Variante der Perl-Regex-Syntax, aber es gibt immer kleine Unterschiede.
      Dennoch sind die Bedeutung von $ und die Art, in den Mehrzeilenmodus zu wechseln, normalerweise recht konsistent.
    • Interessanterweise wurde RFC 9485 https://datatracker.ietf.org/doc/rfc9485/ „I-Regexp: An Interoperable Regular Expression Format“ erst im vergangenen Oktober veröffentlicht.
  • Die Leute verwechseln Zeichenketten und Zeilen.
    Eine Zeichenkette ist eine Sequenz von Zeichen, und eine Zeile kann man auf zwei Arten betrachten.
    Wenn man den Zeilenumbruch als Zeilenabschluss betrachtet, ist eine Zeile null oder mehr Nicht-Zeilenumbruch-Zeichen plus ein Zeilenumbruch; ohne Zeilenumbruch am Ende ist sie keine vollständige Zeile.
    POSIX verwendet diese Sichtweise.
    Wenn man den Zeilenumbruch als Zeilentrenner betrachtet, ist eine Zeile eine Sequenz aus null oder mehr Nicht-Zeilenumbruch-Zeichen.
    In beiden Fällen endet der Inhalt der Zeile vor dem Zeilenumbruch.
    Die Semantik von ^ und $ ist, ob im Einzeilen- oder Mehrzeilenmodus, zeilenbasiert.
    Für zeichenkettenbasierte Semantik, oder beim Umgang mit Dateien gegebenenfalls Semantik für die gesamte Datei, sollte man \A und \Z beziehungsweise das jeweilige Äquivalent verwenden.
    Beide Interpretationen haben ihre Vorteile.
    Wenn Text über eine serielle Verbindung übertragen wird, lässt sich mit Zeilenumbrüchen als Zeilenabschluss gut erkennen, ob eine vollständige Zeile empfangen wurde.
    Bei Textdateien kann es praktischer sein, Zeilenumbrüche als Zeilentrenner zu betrachten, damit die letzte Zeile nicht in einem fehlerhaften Zustand ist; mit Zeilenabschlüssen lassen sich jedoch unvollständig geschriebene Zeilen erkennen.

  • Dadurch gab es in Ruby-basierten Apps mehrfach schwerwiegende Bugs.
    Man sollte immer \A\z verwenden.
    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