Ruby中奇怪的反斜杠替换

32

我不理解这段Ruby代码:

>> puts '\\ <- single backslash'
# \ <- single backslash

>> puts '\\ <- 2x a, because 2 backslashes get replaced'.sub(/\\/, 'aa')
# aa <- 2x a, because two backslashes get replaced

到目前为止,一切都符合预期。但是如果我们使用/ \ / \ / 搜索1,并用'\\\\'编码的2替换,为什么会得到这个:

<code>>> puts '\\ <- only 1 ... replace 1 with 2'.sub(/\\/, '\\\\')
# \ <- only 1 backslash, even though we replace 1 with 2
</code>

然后,当我们使用'\\\\\\'对3进行编码时,我们只得到2:

<code>>> puts '\\ <- only 2 ... 1 with 3'.sub(/\\/, '\\\\\\')
# \\ <- 2 backslashes, even though we replace 1 with 3
</code>

有人能理解为什么反斜杠会在替换字符串中被吞掉吗?这种情况在1.8和1.9上都会发生。

5个回答

78

快速回答

如果您想避免所有这些混乱,请使用更少引起困惑的块语法。以下是一个示例,将每个反斜杠替换为2个反斜杠:

"some\\path".gsub('\\') { '\\\\' }

可怕的细节

问题在于,在使用sub(以及gsub)时,如果没有块,则Ruby会解释替换参数中的特殊字符序列。不幸的是,sub使用反斜杠作为这些字符的转义字符:

\& (the entire regex)
\+ (the last group)
\` (pre-match string)
\' (post-match string)
\0 (same as \&)
\1 (first captured group)
\2 (second captured group)
\\ (a backslash)
像任何其他的转义一样,这会带来一个明显的问题。如果你想在输出字符串中包含上述序列的字面值(例如\1),则需要对其进行转义。所以,要获得Hello \1,你需要将替换字符串设为Hello \\1。然后,在Ruby中表示它作为一个字符串字面量,你需要再次转义那些反斜杠,像这样:"Hello \\\\1"
因此,有两个不同的转义步骤。第一个步骤接受字符串字面量并创建内部字符串值。第二个步骤接受该内部字符串值并用匹配数据替换上述序列。
如果反斜杠后面没有与上述序列之一匹配的字符,则反斜杠(和随后的字符)将未经修改地传递。这也影响到了字符串末尾的反斜杠——它将未经修改地传递。在rubinius代码中可以最容易地看到这种逻辑;只需查找String类中的to_sub_replacement方法。 以下是String#sub解析替换字符串的一些示例: - 1个反斜杠 \(��字符串字面值为"\\"
因为反斜杠位于字符串末尾,并且在它之后没有字符,所以它将未经修改地传递。
结果:\ - 2个反斜杠 \\(其字符串字面值为"\\\\"
这对反斜杠匹配了转义的反斜杠序列(参见\\以上),并被转换为单个反斜杠。
结果:\ - 3个反斜杠 \\\(其字符串字面值为"\\\\\\"
前两个反斜杠匹配\\序列并被转换为单个反斜杠。然后最后一个反斜杠位于字符串末尾,因此它将未经修改地传递。
结果:\\ - 4个反斜杠 \\\\(其字符串字面值为"\\\\\\\\"
两对反斜杠都匹配\\序列并被转换为单个反斜杠。
结果:\\ - 中间带有字符的2个反斜杠 \a\(其字符串字面值为"\\a\\"\a不匹配任何转义序列,因此允许其未经修改地传递。末尾的反斜杠也被允许通过。
结果:\a\ 注意:同样的结果可以从\\a\\(带有字符串字面值:"\\\\a\\\\")获得。
回想一下,如果String#sub使用了不同的转义字符,这可能会更少令人困惑。那么就不需要双倍转义所有反斜杠了。

对于捕获组,我最多只能匹配到9个组,然后会得到一组1、0和1等的匹配...例如,假设第一个组匹配是“<div”,第10个组是“data-loading”,而我得到的结果是“<div0”(第一次匹配结果后面跟着0)。 - Hbksagar
1
很遗憾,这些信息在Ruby文档中完全没有包含(例如http://ruby-doc.org/core-2.1.4/String.html#method-i-gsub)。它应该被包括进去。 - Franco
@Peter 这应该是被选中的答案。同时,我的想法被震撼了。 - a paid nerd

18

问题在于反斜杠(\)用作正则表达式和字符串的转义字符。您可以使用特殊变量\&来减少gsub替换字符串中的反斜杠数量。

foo.gsub(/\\/,'\&\&\&') #for some string foo replace each \ with \\\

编辑:我应该提到,\& 的值来自于一个正则表达式匹配,此处为单个反斜杠。

此外,我以为有一种特殊的方法可以创建一个禁用转义字符的字符串,但显然不是这样的。这些方法都不会产生两个斜杠:

puts "\\"
puts '\\'
puts %q{\\}
puts %Q{\\}
puts """\\"""
puts '''\\'''
puts <<EOF
\\
EOF  

嗯,有趣的方法。这种方法不太“纯粹”,因为如果你有一个更复杂的搜索它就不起作用了。但绝对少了一些字符... - Peter

4

啊,我刚刚打完这些文字后,才意识到\用于引用替换字符串中的组。这意味着在替换字符串中需要一个字面上的\\来获取一个被替换的\。为了获得一个字面上的\\,你需要四个\,因此要将一个替换为两个,实际上需要八个(!)。

# Double every occurrence of \. There's eight backslashes on the right there!
>> puts '\\'.sub(/\\/, '\\\\\\\\')

我有遗漏的地方吗?还有更有效的方法吗?


我认为你是正确的。但是Welch的方法对我来说似乎更好。 - pierrotlefou

4

我来澄清一下作者第二行代码的一点困惑。

你说:

>> puts '\\ <- 2x a, because 2 backslashes get replaced'.sub(/\\/, 'aa')
# aa <- 2x a, because two backslashes get replaced

这里没有替换2个反斜杠。你正在用两个a(“aa”)替换一个转义反斜杠。也就是说,如果您使用.sub(/\\/, 'a'),您只会看到一个'a'。

'\\'.sub(/\\/, 'anything') #=> anything

抱歉,完全正确。那更像是打字错误而不是误解。 - Peter

2

《Pickaxe》一书实际上提到了这个问题。这里有另一种选择(来自最新版的第130页)。

str = 'a\b\c'               # => "a\b\c"
str.gsub(/\\/) { '\\\\' }   # => "a\\b\\c"

好抓住,文档水平神级别。 - G. I. Joe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接