Gsub produciendo resultados diferentes ejecutando el mismo código

Quizás esta sea una pregunta para Stack Overflow y no entiendo qué hace gsub, pero esto parece un comportamiento extraño de Ruby que me hace preguntarme si podría ser algo relacionado con la imagen de Ruby. Obtengo los mismos resultados en irb en mi máquina local.

Pensé que podría tratarse de algún comportamiento de dup que no estaba entendiendo, pero puedo replicar el comportamiento si defino la cadena dos veces. La primera vez, gsub no logra insertar la URL, pero las ejecuciones posteriores de los mismos datos la incluyen como espero.

[1] pry(main)> save=%(<p>On Android, click the three-dot icon in the upper right corner and select Relations from the popup menu. This function wasn't working for me until yesterday, so perhaps on Android, it's still in A/B testing. <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>On Android, click the three-dot icon in the upper right corner and select Relations from the popup menu. This function wasn't working for me until yesterday, so perhaps on Android, it's still in A/B testing. <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=save.dup
=> "<p>On Android, click the three-dot icon in the upper right corner and select Relations from the popup menu. This function wasn't working for me until yesterday, so perhaps on Android, it's still in A/B testing. <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"
[3] pry(main)> s.gsub!(/<UPL-IMAGE-PREVIEW url="(.+?)">.+?<\/UPL-IMAGE-PREVIEW>/i,"\nIMAGEISHERE\n#{$1}\n")
=> "<p>On Android, click the three-dot icon in the upper right corner and select Relations from the popup menu. This function wasn't working for me until yesterday, so perhaps on Android, it's still in A/B testing. \nIMAGEISHERE\n\n</p>\n</r>\n"
[4] pry(main)> s=save.dup
=> "<p>On Android, click the three-dot icon in the upper right corner and select Relations from the popup menu. This function wasn't working for me until yesterday, so perhaps on Android, it's still in A/B testing. <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>On Android, click the three-dot icon in the upper right corner and select Relations from the popup menu. This function wasn't working for me until yesterday, so perhaps on Android, it's still in A/B testing. \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"

Las ejecuciones posteriores de

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

producen el reemplazo de la URL como espero.

Estaba teniendo un problema similar el otro día que describí en este post, pero en una edición anterior. Ese 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

Y lo que sucedía era que tpost.raw en la segunda iteración del bucle estaba tomando el valor del último post.raw de la iteración anterior.

Pensé que me estaba volviendo loco, pero he visto esto

Aquí hay una extrañeza más sencilla. Esta versión tiene gsub que no funciona en la primera ejecución, pero sí en la segunda:

[1] pry(main)> s=%(<p>En Android, haz clic en el icono de tres puntos en la esquina superior derecha y selecciona Relaciones en el menú emergente. Esta función no funcionaba para mí hasta ayer, así que quizás en Android todavía está en pruebas 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>En Android, haz clic en el icono de tres puntos en la esquina superior derecha y selecciona Relaciones en el menú emergente. Esta función no funcionaba para mí hasta ayer, así que quizás en Android todavía está en pruebas 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>En Android, haz clic en el icono de tres puntos en la esquina superior derecha y selecciona Relaciones en el menú emergente. Esta función no funcionaba para mí hasta ayer, así que quizás en Android todavía está en pruebas 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>En Android, haz clic en el icono de tres puntos en la esquina superior derecha y selecciona Relaciones en el menú emergente. Esta función no funcionaba para mí hasta ayer, así que quizás en Android todavía está en pruebas 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"

Las variables como $1 se asignan después de que se realiza una coincidencia de expresión regular. Simplificando un poco tu ejemplo:

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

La expresión "replacement#{$1}" se evalúa antes de que se llame a la función gsub. Por lo tanto, $1 contendrá el valor residual de alguna tarea de expresión regular anterior. (Por eso tu segundo intento funciona: toma $1 del primer intento).

Hay varias opciones para resolver este problema. gsub ofrece muchas funcionalidades diferentes.

Mi preferencia es pasar un bloque a gsub. El bloque se evalúa después de que se ha realizado la coincidencia de la expresión regular, por lo que $1 funcionará como esperas:

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

O bien, puedes utilizar la característica de «referencia trasera» de gsub. No soy fanático de esta sintaxis, pero funciona. En lugar de usar "replacement#{$1}", usa 'replacement\\1' (o \\2, \\3, etc.).

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

¡Dios mío! Gracias. Pensé que me estaba volviendo loco.

Brillante.

Siempre me había preguntado por esas barras invertidas. Cinco años después, lo entiendo.

Esto era totalmente una pregunta de Stack Exchange. :man_shrugging:

Un millón de gracias.