3 pontos por GN⁺ 2024-03-21 | 1 comentários | Compartilhar no WhatsApp
  • Em Python re, $ pode corresponder não apenas ao fim da string, mas também antes da última quebra de linha no final, mesmo com o modo multilinha desativado
  • O fato de ^ parecer significar “início da string” não quer dizer que $ funcione de forma totalmente simétrica; seu significado real varia conforme a implementação da expressão regular
  • Os resultados de $, \z e \Z para "cat\n" diferem entre PHP, ECMAScript, Python, Go, Java 8, .NET 7.0 e Rust, e o \z do Python foi adicionado recentemente no Python 3.14
  • Se for aceitável permitir a quebra de linha final, então o $ em modo multilinha faz correspondência com "cat\n" em todas as plataformas da tabela, mas para corresponder apenas ao fim sem incluir a quebra de linha, a escolha da sintaxe muda
  • Se a quebra de linha final não deve ser correspondida, na maioria das plataformas deve-se usar \z; já no Python anterior ao 3.14 e no ECMAScript, é preciso considerar alternativas diferentes

Onde $ faz correspondência no re do Python

  • No módulo de expressões regulares re do Python, $ pode corresponder ao fim da string ou imediatamente antes da última quebra de linha no final da string, mesmo com o modo multilinha desativado
  • cat$ corresponde a "lolcat" e não corresponde a "internet cat video", então pode parecer simples, mas se houver uma quebra de linha no final, como em "cat\n", o comportamento pode ser diferente do esperado
  • Quando re.MULTILINE é especificado, $ corresponde ao fim da string e ao fim de cada linha, ou seja, imediatamente antes de cada quebra de linha
  • Mesmo no comportamento padrão, $ corresponde ao fim da string e, se houver uma quebra de linha no final, também imediatamente antes dessa quebra

Fazendo correspondência sem incluir a quebra de linha final

  • Para corresponder estritamente apenas ao fim da string, usar só $ pode não ser suficiente; \z e \Z passam a ser candidatos como âncoras de fim
  • Com base na documentação de expressões regulares do Python e em outra explicação sobre sintaxe de expressões regulares, o suporte e o significado de \z e \Z variam conforme a implementação
  • A diferença para "cat\n" é a seguinte
    • PHP: "cat$" corresponde com ou sem multilinha, "cat\z" não corresponde, e "cat\Z" corresponde
    • ECMAScript: "cat$" em multilinha corresponde, "cat$" sem multilinha não corresponde, e \z e \Z não são suportados
    • Python: "cat$" corresponde com ou sem multilinha, e "cat\z" e "cat\Z" não correspondem a "cat\n"
    • Go e Rust: "cat$" em multilinha corresponde, "cat$" sem multilinha e "cat\z" não correspondem, e \Z não é suportado
    • Java 8 e .NET 7.0: "cat$" corresponde com ou sem multilinha, "cat\z" não corresponde, e "cat\Z" corresponde
  • O \z do Python foi adicionado recentemente no Python 3.14; em versões anteriores, ele não era suportado
  • Se for aceitável permitir a quebra de linha final, o $ em modo multilinha corresponde a "cat\n" de forma consistente em todas as plataformas da tabela
  • Se você não quiser corresponder à quebra de linha final, na maioria das plataformas deve usar \z; no Python anterior ao 3.14, deve usar \Z; no ECMAScript, deve usar $ sem multilinha
  • Os dados da tabela foram coletados em regex101.com e não foram testados em runtimes reais

1 comentários

 
GN⁺ 2024-03-21
Comentários do Hacker News
  • Há muito tempo penso em ^ como “início da linha” e em $ como “fim da linha”
    Ao lidar com expressões regulares, muitas vezes processamos texto linha por linha, então o resultado costuma ser o mesmo, mas a forma como penso nesse operador ainda está mais próxima de “linha” do que de “string”
    Provavelmente isso vem muito da influência de ter conhecido regex por meio do grep, criando o hábito de ver a entrada como linhas, não como uma string

    • Eu também, ao ver o título, pensei: “claro que não, onde ouviram isso?”
      Uso expressões regulares há quase 20 anos, mas acho que nunca tinha ouvido dizer que $ é o fim da string; sempre considerei como fim da linha
    • Me incomoda o texto chamar ^ de “início da string”
      Na prática, assim como $ é “fim da linha”, ^ também é “início da linha”; o início da string parece ser mais \A, e o fim da string, \Z
    • Eu também pensava assim, mas testei diretamente em Perl e $ basicamente se comporta como uma asserção positiva de lookahead para o fim da string
      Ele não casa nem consome o caractere de quebra de linha
      Só no modo multilinha ele casa com posições de quebra de linha, mas mesmo assim parece não consumi-las
      Na prática, usando $, não consegui criar uma regex que capturasse o último caractere de uma linha, consumisse a quebra de linha e depois capturasse o primeiro caractere da linha seguinte; o grupo de captura simplesmente termina em $
    • Para mim, foi mais o Vim do que o grep que implantou essa percepção
  • Expressões regulares POSIX e expressões regulares do Python são diferentes
    Em geral, a sintaxe de regex não é universal; é preciso consultar a documentação da implementação em uso
    Segundo o capítulo 9 do POSIX, expressões regulares operam sobre strings, mas alguns utilitários limitam o processamento a linhas
    Além disso, $ é descrito como uma âncora fixada ao fim da string alvo da correspondência; portanto, no fim das contas, se $ significa fim da string ou fim da linha depende do utilitário ou do modo
    Ferramentas comuns como grep, sed, awk e Python operam por padrão linha por linha, então normalmente tratam isso como fim da linha
    Não existe uma única sintaxe universal de expressões regulares
    Sem saber qual linguagem e quais opções estão sendo usadas, não dá para ler ou escrever regex de forma confiável
    https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...

  • Este tema é perfeito para apresentar Robert Elder a quem ainda não o conhece
    Ele produz bom conteúdo no YouTube e no blog, e na série sobre expressões regulares se aprofunda bastante nas diferenças de comportamento entre regex implementadas por várias ferramentas
    O vídeo recente também é bom: https://www.youtube.com/watch?v=ys7yUyyQA-Y
    Há bastante conteúdo que leitores do HN provavelmente achariam interessante, e ele também aborda temas como a realidade e as dificuldades de consultoria
    https://www.youtube.com/@RobertElderSoftware
    https://blog.robertelder.org/
    https://blog.robertelder.org/regular-expressions/
    https://www.youtube.com/watch?v=cK87ktENPrI

  • Quando aprendi Perl, expressões regulares foram uma das primeiras coisas que realmente internalizei, e até hoje Perl ocupa um cantinho confortável na minha mente graças ao livro “Camel”
    Hoje, o conhecimento mais importante é que varia conforme a implementação, então criei o hábito de abrir a tabela de referência correspondente sempre que vou fazer algo
    Por exemplo, regex do Emacs não aceita caracteres de palavra no formato \w e exige uma classe de caracteres parecida com \s_-, o que é irritante, mas acho que o Emacs é imbatível em documentação e facilidade de descoberta
    Alguns utilitários exigem escape em parênteses e outros não; às vezes esse comportamento é configurável, às vezes não
    Depois de passar por todas as fases de confusão, irritação e negação, agora eu simplesmente aceito
    O conceito é o mesmo em qualquer lugar, mas os dialetos mudam

    • Minha cabeça pensa em regex de Perl e depois traduz para as partes inconsistentes da linguagem que estou usando
      Especialmente no shell, em vez de ficar pensando se sed/grep/awk são GNU ou BSD, é muito mais comum eu enfiar perl no pipeline
    • Fico curioso para saber como você internalizou isso
      Perl parece um gato pisando no teclado
  • Quase consigo ouvir inúmeros gerentes de contratação ruins adicionando “como você casa o fim de uma string em uma regex?” à lista de perguntas-armadilha

  • É estranho deixar Perl de fora de uma lista sobre expressões regulares
    Na documentação perlre, $ é descrito assim: casa com o fim da string, ou antes de uma quebra de linha no fim da string, ou, com /m, antes de qualquer quebra de linha

    • Deixar de fora Perl, que pode ser considerado a linguagem mais fortemente associada a expressões regulares, parece uma omissão bem grande
      Talvez isso também indique o quanto Perl saiu do radar hoje em dia
  • Raku, o antigo Perl 6, definiu ^ e $ como início/fim da string e introduziu ^^ e $$ como início/fim de linha
    Não há modo multilinha, nem ele é necessário
    \h também representa espaço em branco horizontal, e \v, espaço em branco vertical
    Por ter sido totalmente repensado e reescrito, teve a vantagem de poder aprender com o fato de que o comportamento antigo surpreendia as pessoas

    • Por isso esta pessoa teimosa não consegue usar Perl 6
      Parece uma mistura aleatória daquela sintaxe parecida com ruído de linha aprendida ao longo de décadas
      Parece claro que o padrão deveria ter sido o contrário
      Teria sido mais natural usar ^ e $ para linhas, e ^^ e $$ para strings
      Porque parece algo como ^^line1$\n^line2$\n^line3$\n$
      Além disso, Perl 6 não está em todo lugar, mas Perl 5 está em todo lugar
    • Eu provavelmente teria escolhido exatamente o contrário
      ^^ parece mais “com cara de início” do que ^
    • Quase todas as regexes que escrevi pressupunham início/fim da string
      Como normalmente se processa passando uma linha para a regex, a escolha de usar ^ e $ simples para a string inteira preserva certa compatibilidade retroativa
  • Fico me perguntando se alguém considera que regex foi padronizada
    Sempre tive que reaprender a cada vez que mudava para um novo ambiente

    • Em certo momento, senti que conhecia todos os dialetos
      Deve haver mais dialetos de regex, mas não esbarro neles, e o que conheço resolve a maior parte dos casos
      É parecido com dirigir um carro alugado
      Ele se comporta um pouco diferente do meu carro, tem recursos ausentes e recursos extras, mas, no geral, a maioria é bem parecida
    • A biblioteca padrão ISO/IEC 14882 C++ exige a implementação de seis sintaxes de regex que são, na prática, padrões oficiais: IEEE Std 1003.1-2008, ou seja, BRE, ERE, awk, grep e egrep do POSIX, além do ECMA-262 EcmaScript 3
      Então, pelo menos eu considero que regex foi padronizada por vários padrões oficiais publicados
      https://open-std.org/jtc1/sc22/…
      https://pubs.opengroup.org/onlinepubs/9699919799/…
      https://262.ecma-international.org/14.0/…
    • As grandes ramificações que conheço são POSIX, Perl/PCRE e mais ou menos RE2, usado no lado do Go
      Muitos sistemas, incluindo JavaScript, implementaram PCRE, porque Perl acrescentou muitas extensões úteis ao sistema POSIX
      Pelo que lembro, RE2 tenta conter os problemas de desempenho e comportamentos peculiares dos sistemas existentes, e eu achava que ele tinha sido implementado inteiro em Go
      Depois descobri que não sabia que RE2 tinha surgido antes de Go
    • Linguagens surgidas depois de Perl em geral usam alguma variação da sintaxe de regex do Perl, mas sempre há pequenas diferenças
      Ainda assim, o significado de $ e a forma de mudar para o modo multilinha costumam ser relativamente consistentes
    • Curiosamente, a RFC 9485 https://datatracker.ietf.org/doc/rfc9485/ “I-Regexp: An Interoperable Regular Expression Format” acabou de ser publicada em outubro do ano passado
  • As pessoas estão confundindo strings e linhas
    Uma string é uma sequência de caracteres, e uma linha pode ser vista de duas formas
    Se a quebra de linha for vista como terminador de linha, uma linha é formada por zero ou mais caracteres que não sejam quebra de linha, seguidos por uma quebra de linha; sem quebra de linha no final, não é uma linha completa
    POSIX usa essa visão
    Se a quebra de linha for vista como separador de linhas, uma linha é uma sequência de zero ou mais caracteres que não sejam quebra de linha
    Em qualquer uma das visões, o conteúdo da linha termina antes da quebra de linha
    A semântica de ^ e $ é baseada em linhas, tanto no modo de linha única quanto no modo multilinha
    Para semântica baseada em string — ou, ao lidar com arquivos, algo que pode ser visto como semântica do arquivo inteiro — deve-se usar \A e \Z, ou equivalentes
    Ambas as interpretações têm vantagens
    Ao transmitir texto por uma conexão serial, usar a quebra de linha como terminador ajuda a saber se uma linha completa foi recebida
    Em arquivos de texto, ver a quebra de linha como separador de linhas pode ser conveniente para que a última linha não fique em estado inválido, mas usar terminadores de linha permite detectar uma linha escrita de forma incompleta

  • Por causa disso, houve algumas falhas graves em apps baseados em Ruby
    Deve-se sempre 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