Gsub으로 동일한 코드를 실행해도 결과가 다른 경우

아마도 이건 Stack Overflow에 질문해야 할 것 같은 내용이고 gsub가 무엇을 하는지 이해하지 못하고 있는 건지도 모르겠습니다~~, 하지만 이건 Ruby의 이상한 동작처럼 보이는데, 혹시 Ruby 이미지 때문일 수도 있을지 궁금합니다~~ 로컬 머신의 irb에서도 동일한 결과를 얻습니다.

dup의 어떤 동작 때문인 것 같다고 생각했지만, 문자열을 두 번 정의하면 이 동작을 재현할 수 있습니다. 첫 번째 실행에서는 gsub가 URL을 삽입하지 못하지만, 동일한 데이터로 이후에 실행하면 예상대로 URL이 포함됩니다.

[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 u
ntil 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-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>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-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>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-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>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-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"
[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"

이후의 실행에서는

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

예상대로 URL 치환이 수행됩니다.

예상대로 작동합니다.

며칠 전에도 비슷한 문제를 겪었는데, 이 게시물에서 설명했듯이 이전 편집 시에 발생한 문제였습니다. 그 당시 코드는 다음과 같았습니다.

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

그때 일어난 일은 루프의 두 번째 반복에서 tpost.raw가 이전 반복의 마지막 post.raw 값을 가져오는 것이었습니다.

정신이 나가는 줄 알았는데, 이런 현상을 목격한 적이 있습니다

더 단순한 특이한 사례가 있습니다. 이 버전에서는 첫 실행 시 gsub가 작동하지 않지만, 두 번째 실행 시에는 작동합니다:

[1] pry(main)> s=%(<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.gsub(/<UPL-IMAGE-PREVIEW url="(.+?)">.+?<\/UPL-IMAGE-PREVIEW>/mi,"\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"
[3] pry(main)> s.gsub(/<UPL-IMAGE-PREVIEW url="(.+?)">.+?<\/UPL-IMAGE-PREVIEW>/mi,"\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"


$1와 같은 변수는 정규식 매칭이 수행된 이후에 값이 채워집니다. 예시를 조금 단순화하면 다음과 같습니다:

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

"replacement#{$1}" 식은 gsub 함수가 호출되기 전에 평가됩니다. 따라서 $1은 이전의 어떤 정규식 작업에서 남게 된 값이 됩니다. (그것이 두 번째 시도가 작동하는 이유입니다 — 첫 번째 시도의 $1을 가져오기 때문이죠)

이 문제를 해결하는 몇 가지 옵션이 있습니다. gsub에는 다양한 기능이 있습니다.

제 선호도는 gsub에 블록을 전달하는 것입니다. 블록은 정규식이 매칭된 후에 평가되므로 $1은 예상대로 작동합니다:

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

또는 gsub의 ‘백레퍼런스(backreference)’ 기능을 사용할 수도 있습니다. 이 문법은 좋아하지 않지만, 작동은 합니다. "replacement#{$1}" 대신 'replacement\1' (또는 \2, \3 등)를 사용합니다.

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

아, 정말 감사합니다. 제정신이 아니었던 줄 알았어요.

정말 훌륭하네요.

항상 그 백슬래시가 궁금했는데, 5년 만에 드디어 이해했습니다.

이건 정말 스택 익스체인지(Stack Exchange)에 질문할 만한 내용이었습니다. :man_shrugging:

정말 감사합니다.