如何在Ruby中的其他正则表达式中嵌入正则表达式

我有一个字符串:

'A Foo' 

并希望在其中找到“Foo”。

我有一个正则表达式:

 /foo/ 

我正在嵌入另一个不区分大小写的正则表达式,所以我可以按步骤构建模式:

 foo_regex = /foo/ pattern = /A #{ foo_regex }/i 

但它不会正确匹配:

 'A Foo' =~ pattern # => nil 

如果我将文本直接嵌入到模式中,它可以工作:

 'A Foo' =~ /A foo/i # => 0 

怎么了?

从表面上看,将模式嵌入到另一个模式中似乎只会起作用,但这是基于模式在Ruby中如何工作的错误假设,它们只是字符串。 使用:

 foo_regex = /foo/ 

创建一个Regexp对象:

 /foo/.class # => Regexp 

因此,它了解用于创建它的可选标志:

 ( /foo/ ).options # => 0 ( /foo/i ).options # => 1 ( /foo/x ).options # => 2 ( /foo/ix ).options # => 3 ( /foo/m ).options # => 4 ( /foo/im ).options # => 5 ( /foo/mx ).options # => 6 ( /foo/imx ).options # => 7 

或者,如果你喜欢二进制:

 '%04b' % ( /foo/ ).options # => "0000" '%04b' % ( /foo/i ).options # => "0001" '%04b' % ( /foo/x ).options # => "0010" '%04b' % ( /foo/xi ).options # => "0011" '%04b' % ( /foo/m ).options # => "0100" '%04b' % ( /foo/mi ).options # => "0101" '%04b' % ( /foo/mx ).options # => "0110" '%04b' % ( /foo/mxi ).options # => "0111" 

并且无论何时使用Regexp,无论是作为独立模式还是嵌入到另一个模式中,都会记住它们。

如果我们在嵌入后查看模式的样子,您可以看到这一点:

 /#{ /foo/ }/ # => /(?-mix:foo)/ /#{ /foo/i }/ # => /(?i-mx:foo)/ 

?-mix:?i-mx:这些选项是如何在嵌入模式中表示的。

根据选项的Regexp文档:

imx也可以使用(?on-off)构造应用于子表达式级别,该构造启用选项,并禁用括号括起来的表达式的选项。

因此,Regexp正在记住这些选项,即使在外部模式中,也会导致整体模式失败:

 pattern = /A #{ foo_regex }/i # => /A (?-mix:foo)/i 'A Foo' =~ pattern # => nil 

可以确保所有子表达式与其周围的模式匹配,但是这很快就会变得过于复杂或混乱:

 foo_regex = /foo/i pattern = /A #{ foo_regex }/i # => /A (?i-mx:foo)/i 'A Foo' =~ pattern # => 0 

相反,我们有source方法返回模式的文本:

 /#{ /foo/.source }/ # => /foo/ /#{ /foo/i.source }/ # => /foo/ 

使用其他Regexp方法(例如union时,嵌入式模式记住选项的问题也会出现:

 /#{ Regexp.union(%w[ab]) }/ # => /(?-mix:a|b)/ 

再次, source可以帮助:

 /#{ Regexp.union(%w[ab]).source }/ # => /a|b/ 

知道这一切:

 foo_regex = /foo/ pattern = /#{ foo_regex.source }/i # => /foo/i 'A Foo' =~ pattern # => 2 

“怎么了?”

关于如何插入Regexp假设是错误的。

通过在插值对象上调用to_s来完成#{...}的插值:

 d = Date.new(2017, 9, 8) #=> # d.to_s #=> "2017-09-08" "today is #{d}!" #=> "today is 2017-09-08!" 

而不仅仅是字符串文字,还有正则表达式文字:

 /today is #{d}!/ #=> /today is 2017-09-08!/ 

在您的示例中,要插入的对象是Regexp

 foo_regex = /foo/ 

并且Regexp#to_s返回:

[…]正则表达式及其选项使用(?opts:source)表示法。

 foo_regex.to_s #=> "(?-mix:foo)" 

因此:

 /A #{foo_regex}/i #=> /A (?-mix:foo)/i 

就像:

 "A #{foo_regex}" #=> "A (?-mix:foo)" 

换句话说:由于Regexp#to_s的实现方式,您可以在不丢失标志的情况下插入模式。 这是一个function,而不是一个bug。

如果Regexp#to_s只返回源(没有选项),它将按照您期望的方式工作:

 def foo_regex.to_s source end /A #{foo_regex}/i #=> /A foo/i 

以上代码仅用于演示目的,请勿执行此操作。