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?