3 puntos por GN⁺ 2024-03-21 | 1 comentarios | Compartir por WhatsApp
  • En re de Python, $ puede coincidir no solo con el fin de la cadena, sino también con la posición antes del último salto de línea al final, incluso si el modo multilínea está desactivado
  • Aunque ^ parezca significar “inicio de la cadena”, no conviene asumir que $ se comporta de forma completamente simétrica; su significado real varía según la implementación de expresiones regulares
  • Los resultados de $, \z y \Z para "cat\n" difieren entre PHP, ECMAScript, Python, Go, Java 8, .NET 7.0 y Rust, y \z de Python se agregó recién en Python 3.14
  • Si se permite el salto de línea final, $ en modo multilínea coincide con "cat\n" en todas las plataformas de la tabla, pero si se quiere coincidir solo con el final excluyendo el salto de línea, la elección de la sintaxis cambia
  • Si no se debe coincidir con el último salto de línea, en la mayoría de las plataformas se usa \z, y en versiones anteriores a Python 3.14 y en ECMAScript hay que considerar alternativas distintas

Dónde coincide $ en re de Python

  • En el módulo de expresiones regulares re de Python, $ puede coincidir con el fin de la cadena o justo antes del último salto de línea al final de la cadena, incluso si el modo multilínea está desactivado
  • cat$ parece simple porque coincide con "lolcat" y no coincide con "internet cat video", pero si hay un salto de línea al final, como en "cat\n", puede comportarse distinto de lo esperado
  • Cuando se especifica re.MULTILINE, $ coincide con el fin de la cadena y con el final de cada línea, es decir, justo antes de cada salto de línea
  • Incluso con el valor predeterminado, $ coincide con el fin de la cadena y, si hay un salto de línea al final de la cadena, también coincide justo antes de ese salto de línea

Coincidir excluyendo el último salto de línea

  • Para coincidir estrictamente solo con el fin de la cadena, puede que $ no sea suficiente, y \z y \Z pasan a ser candidatos como anclas de fin
  • Según la documentación de expresiones regulares de Python y otras explicaciones de sintaxis de expresiones regulares, el soporte y el significado de \z y \Z varían según la implementación
  • Las diferencias para "cat\n" son las siguientes
    • PHP: "cat$" coincide independientemente de si está en modo multilínea; "cat\z" no coincide, y "cat\Z" sí coincide
    • ECMAScript: "cat$" en modo multilínea coincide; "cat$" sin modo multilínea no coincide, y \z y \Z no son compatibles
    • Python: "cat$" coincide independientemente de si está en modo multilínea, y "cat\z" y "cat\Z" no coinciden con "cat\n"
    • Go y Rust: "cat$" en modo multilínea coincide; "cat$" sin modo multilínea y "cat\z" no coinciden, y \Z no es compatible
    • Java 8 y .NET 7.0: "cat$" coincide independientemente de si está en modo multilínea; "cat\z" no coincide, y "cat\Z" sí coincide
  • \z de Python se agregó recién en Python 3.14, y no era compatible en versiones anteriores
  • Si se permite el salto de línea final, $ en modo multilínea coincide de forma consistente con "cat\n" en todas las plataformas de la tabla
  • Para no coincidir con el salto de línea final, en la mayoría de las plataformas se usa \z; en versiones anteriores a Python 3.14 se debe usar \Z, y en ECMAScript se debe usar $ sin modo multilínea
  • Los datos de la tabla se recopilaron en regex101.com y no se probaron en runtimes reales

1 comentarios

 
GN⁺ 2024-03-21
Comentarios de Hacker News
  • Desde hace mucho he pensado en ^ como el "inicio de línea" y en $ como el "final de línea"
    Al trabajar con expresiones regulares, muchas veces se procesa el texto línea por línea, así que a menudo el resultado termina siendo el mismo, pero la forma en que sigo pensando en esos operadores está más cerca de una "línea" que de una "cadena"
    Supongo que mucho de eso viene de haber conocido las regex con grep, así que me quedó la costumbre de ver la entrada no como una cadena sino como líneas

    • Yo también, al ver el título, pensé: "claro que no, ¿de dónde salió esa idea?"
      Llevo casi 20 años usando regex, pero creo que es la primera vez que escucho decir que $ es el final de la cadena; siempre lo he considerado el final de línea
    • Me hace ruido que en el artículo se describa ^ como el "inicio de la cadena"
      En la práctica, así como $ es el "final de línea", ^ también es el "inicio de línea", y el inicio de la cadena se parece más a \A, mientras que el final de la cadena se parece más a \Z
    • Yo también pensaba eso, pero al probarlo directamente en Perl, $ por defecto se comporta como una afirmación de anticipación positiva sobre el final de la cadena
      No hace match ni consume el carácter de salto de línea
      Solo en modo multilínea hace match en la posición del salto de línea, pero aun así parece que no lo consume
      De hecho, usando $ no pude construir una regex que capturara el último carácter de una línea, consumiera el salto de línea y luego capturara el primer carácter de la siguiente; el grupo de captura simplemente termina en $
    • Para mí, más que grep, fue Vim lo que me metió esa idea en la cabeza
  • Las regex POSIX y las regex de Python son distintas
    En general, la sintaxis de las regex no es universal, así que hay que revisar la documentación de la implementación que estés usando
    Según el capítulo 9 de POSIX, las regex operan sobre cadenas, pero algunas utilidades limitan el procesamiento a una línea por vez
    Además, $ se define como un ancla fijada al final de la cadena objetivo, así que al final si $ significa fin de cadena o fin de línea depende de la utilidad o del modo
    Herramientas comunes como grep, sed, awk y Python trabajan por defecto línea por línea, así que normalmente se trata como fin de línea
    No existe una única sintaxis de regex universal
    Si no sabes qué lenguaje y qué opciones se están usando, no puedes leer ni escribir regex de forma confiable
    https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...

  • Este tema es perfecto para presentar a Robert Elder a quienes no lo conocen
    Hace muy buen contenido en YouTube y en su blog, y en su serie sobre regex profundiza bastante en las diferencias de comportamiento entre las implementaciones de varios herramientas
    Su video reciente también está bueno: https://www.youtube.com/watch?v=ys7yUyyQA-Y
    Tiene mucho contenido que probablemente interese a lectores de HN, y también toca temas como la realidad y las dificultades de la consultoría
    https://www.youtube.com/@RobertElderSoftware
    https://blog.robertelder.org/
    https://blog.robertelder.org/regular-expressions/
    https://www.youtube.com/watch?v=cK87ktENPrI

  • Cuando aprendí Perl, las regex fueron de las primeras cosas que realmente interioricé, y hasta hoy Perl sigue ocupando un lugar cómodo en mi cabeza gracias al libro "Camel"
    Lo más importante que sé ahora es que depende de la implementación, así que ya tengo el hábito de sacar la hoja de referencia cada vez que voy a trabajar con algo
    Por ejemplo, me molesta que las regex de Emacs no soporten caracteres de palabra del tipo \w y que haya que usar clases de caracteres como \s_-, pero aun así creo que Emacs es de lo mejor en documentación y facilidad de descubrimiento
    Algunas utilidades requieren escapar los paréntesis y otras no, y en algunos casos ese comportamiento se puede configurar y en otros no
    Ya pasé por las etapas de confusión, frustración y negación, y ahora simplemente lo acepto
    El concepto es el mismo en todos lados, pero cambia el dialecto

    • Mi cabeza piensa en regex de Perl, y luego traduzco eso a las inconsistencias del lenguaje que esté usando
      Sobre todo en el shell, antes que ponerme a recordar si sed/grep/awk son GNU o BSD, muchas veces prefiero meter perl en el pipeline
    • Me intriga cómo lograste interiorizarlo
      Perl parece como si un gato hubiera caminado sobre el teclado
  • Casi puedo oír a un montón de malos reclutadores agregando "¿cómo se hace match con el final de una cadena en regex?" a su lista de preguntas trampa

  • Se siente raro dejar fuera a Perl en una lista sobre regex
    En la documentación de perlre, $ se explica así: hace match con el final de la cadena, o con la posición anterior al salto de línea al final de la cadena, o, si usas /m, con la posición anterior a cualquier salto de línea

    • Omitir Perl, que probablemente sea el lenguaje más fuertemente asociado con las regex, parece una omisión bastante grande
      También da la impresión de que eso muestra hasta qué punto Perl ha quedado fuera del foco hoy en día
  • Raku, antes Perl 6, definió ^ y $ como inicio/fin de cadena, e introdujo ^^ y $$ para inicio/fin de línea
    No tiene modo multilínea y tampoco lo necesita
    También tiene \h para espacio horizontal y \v para espacio vertical
    Como se replanteó y reescribió por completo, tuvo la ventaja de poder aprender del hecho de que el comportamiento anterior sorprendía a la gente

    • Por eso este terco no puede usar Perl 6
      Se siente como si hubieran mezclado al azar una sintaxis tipo line noise que uno aprendió durante décadas
      Parecería más claro si el valor predeterminado hubiera sido el contrario
      Habría sido más natural usar ^ y $ para líneas, y ^^ y $$ para cadenas
      Porque se ve como ^^line1$\n^line2$\n^line3$\n$
      Además, Perl 6 no está en todos lados, pero Perl 5 sí
    • Yo habría elegido exactamente al revés
      ^^ se ve más “como inicio” que ^
    • Casi todas las regex que he escrito asumían inicio/fin de cadena
      Normalmente proceso líneas metiéndolas en la regex, así que usar ^ y $ simples para la cadena completa conserva cierta compatibilidad hacia atrás
  • No estoy seguro de que alguien considere que las regex estén estandarizadas
    Cada vez que cambiaba de entorno, siempre tenía que volver a aprenderlas

    • En algún momento sentí que conocía todos los dialectos
      Seguro hay más dialectos de regex, pero no me los he topado y con los que conozco resuelvo casi todo
      Es parecido a manejar un auto rentado
      Funciona un poco distinto a mi coche, le faltan algunas funciones y tiene otras extra, pero en general casi todos se parecen bastante
    • La biblioteca estándar ISO/IEC 14882 de C++ exige implementar seis gramáticas de regex que son, de hecho, estándares de referencia: IEEE Std 1003.1-2008, es decir, BRE, ERE, awk, grep y egrep de POSIX, y EcmaScript 3 de ECMA-262
      Así que al menos yo sí diría que las regex están estandarizadas mediante varios estándares oficiales públicos
      https://open-std.org/jtc1/sc22/…
      https://pubs.opengroup.org/onlinepubs/9699919799/…
      https://262.ecma-international.org/14.0/…
    • Las grandes ramas que conozco son POSIX, Perl/PCRE y RE2, que se usa del lado de Go
      Muchos sistemas, incluido JavaScript, implementaron PCRE, porque Perl añadió muchas extensiones útiles al esquema POSIX
      Según recuerdo, RE2 busca contener los problemas de rendimiento y los comportamientos extraños de los sistemas anteriores, y yo creía que estaba implementado entero en Go
      Después supe que RE2 apareció antes que Go
    • Los lenguajes que surgieron después de Perl por lo general usan alguna variante de la sintaxis de regex de Perl, pero siempre con pequeñas diferencias
      Aun así, el significado de $ y la forma de cambiar al modo multilínea suelen ser bastante consistentes
    • Curiosamente, el RFC 9485 https://datatracker.ietf.org/doc/rfc9485/ “I-Regexp: An Interoperable Regular Expression Format” apenas se publicó en octubre del año pasado
  • La gente está confundiendo cadenas y líneas
    Una cadena es una secuencia de caracteres, y una línea puede verse de dos maneras
    Si el salto de línea se considera terminador de línea, una línea son cero o más caracteres que no son salto de línea seguidos de un salto de línea, y si al final no hay salto de línea no es una línea completa
    POSIX usa esta perspectiva
    Si el salto de línea se considera separador de líneas, una línea es una secuencia de cero o más caracteres que no son salto de línea
    En cualquiera de las dos interpretaciones, el contenido de la línea termina antes del salto de línea
    La semántica de ^ y $ está basada en líneas, tanto en modo de una sola línea como en modo multilínea
    Para semántica basada en cadenas, o en archivos completos si estás tratando archivos, hay que usar \A y \Z o su equivalente
    Ambas interpretaciones tienen ventajas
    Al transmitir texto por conexión serial, tratar el salto de línea como terminador ayuda a saber si ya recibiste una línea completa
    En archivos de texto, ver el salto de línea como separador puede ser más cómodo porque la última línea no queda en un estado inválido, pero tener un terminador de línea permite detectar líneas escritas de forma incompleta

  • Por esto ha habido varios bugs graves en apps basadas en Ruby
    Siempre hay que usar \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