How can I prevent Ruby's inverted character class from matching newline characters?

Viewed 56

TLDR: How can I make Ruby's regex engine exclude newlines from inverted character classes when multi-line matching is disabled?

Background

With most regular expression engines I've used, it is not possible to match across lines unless the multi-line modifier is specified. The . operator should match any character except a newline, and an inverted character class ([^x]) also should not match a newline. For example, with ripgrep:

printf "a\nb\nc\nd" | rg '^a.*b.*c.*d$'
# [no output]

printf "a\nb\nc\nd" | rg '^a[^x]*b[^x]*c[^x]*d$'
# [no output]

The ^ and $ operators should match the start and end of any line, not just the whole string, as shown here:

printf "a\nb\nc\nd" | rg '^c$'
# c

With the multiline option, ripgrep can match across lines:

printf "a\nb\nc\nd" | rg --multiline --multiline-dotall '^a.*b.*c.*d$'
# a
# b
# c
# d

printf "a\nb\nc\nd" | rg --multiline --multiline-dotall '^a[^x]*b[^x]*c[^x]*d$'
# a
# b
# c
# d

Ruby also does not allow . to match newlines unless the m modifier is present, and the ^ and $ operators can match on any line within a string:

"a\nb\nc\nd".match(/^a.*b.*c.*d/)
# => nil

"a\nb\nc\nd".match(/^c$/)
# => #<MatchData "c">

"a\nb\nc\nd".match(/^a.*b.*c.*d/m)
# => #<MatchData "a\nb\nc\nd">

The problem

However, Ruby's regex engine is matching newlines when given an inverted character class, even without the multiline modifier. This is very unexpected!

# This should return nil!
"a\nb\nc\nd".match(/^a[^x]*b[^x]*c[^x]*d$/)
# => #<MatchData "a\nb\nc\nd">

"a\nb\nc\nd".match(/^a[^x]*b[^x]*c[^x]*d$/m)
# => #<MatchData "a\nb\nc\nd">

I've tried Ruby 1.9.3, 2.7.2, and 3.0, and they all exhibit this behavior. So this has been present in Ruby for a very long time.

The question

I frequently use regular expressions when writing specs for something that generates multi-line text. Having to specify \n whenever I use an inverted character class is a pretty big hit to productivity and violates the principle of least astonishment.

So, how can I make Ruby's regex engine exclude newlines from inverted character classes when multi-line matching is disabled?

0 Answers
Related