From 5a158d32a850174912065a07344810964a8b47e0 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Sat, 21 Mar 2026 14:37:10 +0900 Subject: [PATCH] mruby-regexp: support \& \` \' \+ \\ in sub/gsub replacements Replacement strings now support: \& = full match, \` = pre_match, \' = post_match, \+ = last successful capture, \\ = literal backslash. Co-authored-by: Claude --- mrbgems/mruby-regexp/mrblib/string_regexp.rb | 43 +++++++++++++++++--- mrbgems/mruby-regexp/test/regexp.rb | 19 +++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/mrbgems/mruby-regexp/mrblib/string_regexp.rb b/mrbgems/mruby-regexp/mrblib/string_regexp.rb index 28b225ec8..366e14a98 100644 --- a/mrbgems/mruby-regexp/mrblib/string_regexp.rb +++ b/mrbgems/mruby-regexp/mrblib/string_regexp.rb @@ -13,6 +13,41 @@ class String re =~ self end + def __sub_replace(rep, md) + return rep unless rep.include?("\\") + result = "" + i = 0 + while i < rep.length + if rep[i] == "\\" && i + 1 < rep.length + c = rep[i + 1] + case c + when '0'..'9' + result += (md[c.to_i] || "") + when '&' + result += (md[0] || "") + when '`' + result += md.pre_match + when "'" + result += md.post_match + when '+' + # last successful capture + last = nil + md.captures.each { |c| last = c if c } + result += (last || "") + when "\\" + result += "\\" + else + result += "\\" + c + end + i += 2 + else + result += rep[i] + i += 1 + end + end + result + end + def sub(pattern, replacement = nil, &block) pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a?(String) md = pattern.match(self) @@ -23,9 +58,7 @@ class String if block rep = block.call(md[0]).to_s else - rep = replacement.to_s - # handle \0, \1, etc. in replacement string - rep = rep.gsub(/\\(\d)/) { md[$1.to_i] || "" } if rep.include?("\\") + rep = __sub_replace(replacement.to_s, md) end pre + rep + post end @@ -41,9 +74,7 @@ class String if block result += block.call(md[0]).to_s else - rep = replacement.to_s - rep = rep.gsub(/\\(\d)/) { md[$1.to_i] || "" } if rep.include?("\\") - result += rep + result += __sub_replace(replacement.to_s, md) end matched_len = md[0].length if matched_len == 0 diff --git a/mrbgems/mruby-regexp/test/regexp.rb b/mrbgems/mruby-regexp/test/regexp.rb index e5dd7ffd3..d09a8e97e 100644 --- a/mrbgems/mruby-regexp/test/regexp.rb +++ b/mrbgems/mruby-regexp/test/regexp.rb @@ -228,6 +228,25 @@ assert("String#gsub") do assert_equal "h-ll-", "hello".gsub(Regexp.new("[eo]"), "-") end +assert("String#sub with \\& \\` \\' specials") do + # \& = full match + assert_equal "a[bc]d", "abcd".sub(/bc/, '[\\&]') + # \` = pre_match + assert_equal "a[a]d", "abcd".sub(/bc/, '[\\`]') + # \' = post_match + assert_equal "a[d]d", "abcd".sub(/bc/, "[\\']") + # \+ = last capture + assert_equal "a[c]d", "abcd".sub(/(b)(c)/, '[\\+]') + # \\ = literal backslash + assert_equal "a\\d", "abcd".sub(/bc/, "\\\\") + # \1 still works + assert_equal "abbd", "abcd".sub(/(b)c/, '\\1\\1') +end + +assert("String#gsub with \\& special") do + assert_equal "[a][b][c]", "abc".gsub(/./, '[\\&]') +end + assert("String#scan") do assert_equal ["1", "2", "3"], "a1b2c3".scan(Regexp.new("\\d")) end