Gsub produzindo resultados diferentes executando o mesmo código

Talvez essa seja uma pergunta do Stack Overflow e eu não entenda o que gsub faz~~, mas isso parece um comportamento bizarro do Ruby que me faz questionar se poderia ser, de alguma forma, a imagem do Ruby?~~ Eu obtenho os mesmos resultados no irb na minha máquina local.

Eu pensei que isso pudesse ser algum comportamento do dup que eu não estava entendendo, mas consigo replicar o comportamento se eu definir a string duas vezes. A primeira vez, o gsub falha ao inserir a URL, mas execuções subsequentes com os mesmos dados a incluem como eu espero.

[1] pry(main)> save=%(<p>No Android, clique no ícone de três pontos no canto superior direito e selecione Relações no menu pop-up. Essa função não funcionava para mim até ontem, então talvez no Android ainda esteja em teste A/B. <UPL-IMAGE-PREVIEW url="https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85
280-screen-shot-2021-08-29-at-83023-pm.png">[upl-image-preview url=https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08
-29-at-83023-pm.png]</UPL-IMAGE-PREVIEW></p>
[1] pry(main)* </r>
[1] pry(main)* )
=> "<p>No Android, clique no ícone de três pontos no canto superior direito e selecione Relações no menu pop-up. Essa função não funcionava para mim até ontem, então
 talvez no Android ainda esteja em teste A/B. <UPL-IMAGE-PREVIEW url=\"https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2
021-08-29-at-83023-pm.png\">[upl-image-preview url=https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08-29-at-83023-pm.
png]</UPL-IMAGE-PREVIEW></p>\n</r>\n"
[2] pry(main)> s=save.dup
=> "<p>No Android, clique no ícone de três pontos no canto superior direito e selecione Relações no menu pop-up. Essa função não funcionava para mim até ontem, então
 talvez no Android ainda esteja em teste A/B. <UPL-IMAGE-PREVIEW url=\"https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2
021-08-29-at-83023-pm.png\">[upl-image-preview url=https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08-29-at-83023-pm.
png]</UPL-IMAGE-PREVIEW></p>\n</r>\n"
[3] pry(main)> s.gsub!(/<UPL-IMAGE-PREVIEW url="(.+?)">.+?<\/UPL-IMAGE-PREVIEW>/i,"\nIMAGEISHERE\n#{$1}\n")
=> "<p>No Android, clique no ícone de três pontos no canto superior direito e selecione Relações no menu pop-up. Essa função não funcionava para mim até ontem, então
 talvez no Android ainda esteja em teste A/B. \nIMAGEISHERE\n\n</p>\n</r>\n"
[4] pry(main)> s=save.dup
=> "<p>No Android, clique no ícone de três pontos no canto superior direito e selecione Relações no menu pop-up. Essa função não funcionava para mim até ontem, então talvez no Android ainda esteja em teste A/B. <UPL-IMAGE-PREVIEW url=\"https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08-29-at-83023-pm.png\">[upl-image-preview url=https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08-29-at-83023-pm.png]</UPL-IMAGE-PREVIEW></p>\n</r>\n"
[5] pry(main)> s.gsub!(/<UPL-IMAGE-PREVIEW url="(.+?)">.+?<\/UPL-IMAGE-PREVIEW>/i,"\nIMAGEISHERE\n#{$1}\n")
=> "<p>No Android, clique no ícone de três pontos no canto superior direito e selecione Relações no menu pop-up. Essa função não funcionava para mim até ontem, então talvez no Android ainda esteja em teste A/B. \nIMAGEISHERE\nhttps://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08-29-at-83023-pm.png\n</p>\n</r>\n"

Execuções subsequentes de

s=save.dup
s.gsub!(/<UPL-IMAGE-PREVIEW url="(.+?)">.+?<\/UPL-IMAGE-PREVIEW>/i,"\nIMAGEISHERE\n#{$1}\n")

produzem a substituição da URL como eu espero.

funcionam como esperado.

Eu estava tendo um problema semelhante outro dia que descrevi neste post aqui, mas em uma edição anterior. Esse código era

def fix_slack_posts
  SiteSetting.min_post_length = 2
  reg=/(\*\*)(This topic was automatically generated from Slack. You can find the original thread \[here\].+?\))(\*\*\.)?\s*?([a-zA-Z, ()]* : )(.*)/m
  preg = /([a-zA-Z, ()]+? : )(.*)/m
  topic_posts = Post.where("raw like '**This topic was automatically%'")
  topic_posts.each do |tpost|
    begin
      tpost.raw.gsub!(reg,"#{$5}\n\n#{$2}.")
      tpost.save!
      tpost.rebake!
    rescue
      puts "Can't update topic post #{tpost.raw}"
    end
    posts = Post.where(topic_id: tpost.topic_id).where("post_number > 1")
    posts.each do |post|
      if post.raw.gsub(preg,"#{$2}").length>=10
        begin
          post.raw.gsub!(preg,"#{$2}")
          post.save!
          post.rebake!
        rescue
          puts "#{post.id}--cannot save #{post.raw}. "
        end
      end
    end
  end
  SiteSetting.min_post_length = 10
end

E o que estava acontecendo era que o tpost.raw na segunda iteração do loop estava recebendo o valor do último post.raw da iteração anterior.

Eu achei que ia ficar maluco, mas já vi isso

Aqui está uma estranheza mais simples. Esta versão tem o gsub não funcionando na primeira execução, mas funcionando na segunda:

[1] pry(main)> s=%(<p>No Android, clique no ícone de três pontos no canto superior direito e selecione Relações no menu pop-up. Essa função não funcionava para mim até ontem, então talvez no Android ainda esteja em testes A/B. <UPL-IMAGE-PREVIEW url="https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08-29-at-83023-pm.png">[upl-image-preview url=https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08-29-at-83023-pm.png]</UPL-IMAGE-PREVIEW></p>
[1] pry(main)* </r>
[1] pry(main)* )
=> "<p>No Android, clique no ícone de três pontos no canto superior direito e selecione Relações no menu pop-up. Essa função não funcionava para mim até ontem, então talvez no Android ainda esteja em testes A/B. <UPL-IMAGE-PREVIEW url=\"https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08-29-at-83023-pm.png\">[upl-image-preview url=https://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08-29-at-83023-pm.png]</UPL-IMAGE-PREVIEW></p>\n</r>\n"
[2] pry(main)> s.gsub(/<UPL-IMAGE-PREVIEW url="(.+?)">.+?<\/UPL-IMAGE-PREVIEW>/mi,"\nIMAGEISHERE\n#{$1}\n")
=> "<p>No Android, clique no ícone de três pontos no canto superior direito e selecione Relações no menu pop-up. Essa função não funcionava para mim até ontem, então talvez no Android ainda esteja em testes A/B. \nIMAGEISHERE\n\n</p>\n</r>\n"
[3] pry(main)> s.gsub(/<UPL-IMAGE-PREVIEW url="(.+?)">.+?<\/UPL-IMAGE-PREVIEW>/mi,"\nIMAGEISHERE\n#{$1}\n")
=> "<p>No Android, clique no ícone de três pontos no canto superior direito e selecione Relações no menu pop-up. Essa função não funcionava para mim até ontem, então talvez no Android ainda esteja em testes A/B. \nIMAGEISHERE\nhttps://somehost.s3.eu-central-1.amazonaws.com/2021-08-29/1630236738-85280-screen-shot-2021-08-29-at-83023-pm.png\n</p>\n</r>\n"


Variáveis como $1 são preenchidas após a execução de uma correspondência de regex. Simplificando um pouco o seu exemplo:

s.gsub('original(value)', "replacement#{$1}")

A expressão "replacement#{$1}" é avaliada antes de a função gsub ser chamada. Portanto, $1 conterá o valor residual de alguma tarefa de regex anterior. (É por isso que sua segunda tentativa funciona — ela usa o $1 da primeira tentativa)

Existem algumas opções para resolver esse problema. gsub possui diversas funcionalidades.

Minha preferência é passar um bloco para o gsub. O bloco é avaliado após a correspondência do regex, então $1 funcionará como você espera:

s.gsub('original(value)') { |match| "replacement#{$1}" }

Ou você pode usar o recurso de “backreference” do gsub. Não sou fã dessa sintaxe, mas ela funciona. Em vez de usar "replacement#{$1}", use 'replacement\1' (ou \2, \3, etc.)

s.gsub('original(value)', 'replacement\1')

Nossa, muito obrigado. Achei que estava ficando maluco.

Brilhante.

Eu sempre me perguntei sobre aqueles barras invertidas. Cinco anos depois, finalmente entendi.

Isso definitivamente era uma pergunta do Stack Exchange. :man_shrugging:

Um milhão de agradecimentos.