Regular Expressions
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them.
A regular expression is a pattern that describes a set of strings. In Ruby it is an object of class Regexp, created by
a literal such as /\d+/, and it is used in three ways: to test whether a string has a given shape, to extract the pieces
of a string that match, and to transform a string by replacing the parts that match. Ruby’s engine is Onigmo, a fork of
Oniguruma that also powers other languages, so most Perl-style syntax works as expected; this page shows the syntax Ruby
documents, the objects and methods around it (Regexp, MatchData, and the String methods that accept a pattern), the
global variables that hold the last match, and the performance and security properties that matter once a pattern meets
untrusted input.
The page is a reference, not a tutorial on regular expressions in general. Strings themselves are in
Strings, the encodings that affect matching in
Encodings and Unicode, literal syntax such as %r in
Literals, case/when in
Control Expressions, matching a MatchData with case/in in
Pattern Matching, and denial-of-service through patterns in
Security. For how other languages treat the same ideas, see
Go text processing and
Rust strings and text.
Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92
using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings). In the examples,
# => comments show the real value of the expression on their line, in the Ruby 3.4+ inspect format. The examples use the
names of the shelf library catalogue used throughout this section (Shelf, Isbn, Book, Catalog); each file is
self-contained so it can be run on its own.
Overview
| Piece | What it is |
|---|---|
|
A compiled pattern: a source string, option flags, an encoding and an optional timeout. Literals are frozen since Ruby 3.0. |
|
The result of a successful match: the matched text, the captured groups, their positions and the strings around the match. |
|
The usual way to test or extract; |
|
The methods that accept a pattern to find, replace or cut. |
|
The last match of the current method and thread, set by |
|
The Ruby 3.2 tools against regular-expression denial of service. |
Creating regular expressions
Literals, %r and Regexp.new
A regular expression literal is written between slashes, optionally followed by option letters. %r takes any delimiter, which
avoids escaping slashes in paths (%r{/books/(\d+)}). Regexp.new (alias Regexp.compile) builds one from a string at
run time; the second argument is the options, either an Integer combining Regexp::IGNORECASE, Regexp::EXTENDED and
Regexp::MULTILINE, or, since Ruby 3.2, a String of the letters i, m and x. Anything else is rejected: an unknown
letter raises ArgumentError, and true means ignore case for compatibility. A third positional argument, accepted by old
Ruby versions to select an encoding, is no longer accepted.
The literal form is parsed with the program, so a syntax error in a literal pattern is a SyntaxError at load time, while a
bad pattern passed to Regexp.new raises RegexpError when the line runs. A literal is also cheaper: without interpolation it
is compiled once per program, not on every evaluation, and since Ruby 3.0 it is frozen. A regexp made with Regexp.new
is not frozen.
# frozen_string_literal: true
# Three ways to write a regexp literal, and the constructor.
isbn_pattern = /\A\d{3}-\d-\d{2}-\d{6}-\d\z/
path_pattern = %r{\A/books/(\d+)\z}
title_pattern = Regexp.new("harry potter", Regexp::IGNORECASE)
isbn_pattern.inspect # => "/\\A\\d{3}-\\d-\\d{2}-\\d{6}-\\d\\z/"
path_pattern.inspect # => "/\\A\\/books\\/(\\d+)\\z/"
title_pattern.inspect # => "/harry potter/i"
# Interpolation builds a pattern at run time; escape untrusted text first.
user_input = "C++ (2nd ed.)"
Regexp.escape(user_input) # => "C\\+\\+\\ \\(2nd\\ ed\\.\\)"
Regexp.new(Regexp.escape(user_input)).match?("Learning C++ (2nd ed.)") # => true
/#{Regexp.escape(user_input)}/o.source # => "C\\+\\+\\ \\(2nd\\ ed\\.\\)"
# Regexp.union joins alternatives and escapes strings.
Regexp.union("ruby", "go", /r(u|a)st/i) # => /ruby|go|(?i-mx:r(u|a)st)/
Regexp.union(%w[a.b c+d]) # => /a\.b|c\+d/
Regexp.union # => /(?!)/
# Inspecting a regexp.
re = /colou?r/ix
re.source # => "colou?r"
re.to_s # => "(?ix-m:colou?r)"
re.inspect # => "/colou?r/ix"
re.options # => 3
re.casefold? # => true
re.names # => []
Regexp.try_convert("not a regexp") # => nil
Regexp.try_convert(re) # => /colou?r/ix
# A literal is frozen (Ruby 3.0); Regexp.new is not.
/a/.frozen? # => true
Regexp.new("a").frozen? # => false
Regexp.escape (alias Regexp.quote) turns text into a pattern that matches that text literally: it backslash-escapes every
metacharacter (. ? - + * ^ \ | $ ( ) [ ] { }), spaces included. Use it whenever any part of a pattern comes from a
variable that is not itself a pattern. Regexp.union accepts strings (escaped for you), regexps (embedded with their own
flags) or one array of either, and Regexp.union with no arguments returns /(?!)/, a pattern that never matches. Embedding
a Regexp object inside another literal with #{} calls to_s, which wraps it in a group with its own flags, such as
(?i-mx:ruby), so the case-insensitivity of the inner pattern does not leak into the outer one.
Source, options and conversion
Regexp#source returns the pattern text, #options returns the flag integer, #casefold? tells whether i is set,
#names and #named_captures describe the named groups, and #encoding and #fixed_encoding? describe the encoding.
Regexp.try_convert returns its argument if it is a Regexp, calls to_regexp if the object defines it, and otherwise returns
nil. Regexp#inspect gives the literal form; Regexp#to_s gives the embeddable (?flags:source) form. Two regexps are
== and eql? when their source, options and encoding are equal, and they hash alike; two patterns that merely match the same
strings are not equal.
The o modifier and interpolation
A literal with interpolation, /\A#{prefix}/, builds a new Regexp every time it is evaluated. The o modifier makes Ruby
build it once, the first time, and reuse that object forever, even if the interpolated value changes later. That is rarely
what you want today: assign the pattern to a constant or a local variable instead, which says the same thing without the
surprise (see the example under Modes). Interpolation also inserts text, not a pattern object: an
interpolated string containing metacharacters changes the meaning of the pattern, which is why untrusted values must go through
Regexp.escape.
Matching
There are four ways to ask whether a pattern matches. They differ in what they return and in what they record.
| Expression | Returns | Sets last-match globals | Use it for |
|---|---|---|---|
|
|
yes |
extracting captures |
|
index of the match, or |
yes |
a position, or named-group local variables |
|
|
no |
a plain yes/no test (fastest) |
|
|
yes |
|
match and match? take an optional start position (re.match(str, 12) starts searching at character 12), and match yields
the MatchData to a block when it matched and returns the block’s value. =~ with a literal Regexp on the left and
named groups also assigns local variables, one per name (see Named groups). The
operator =~ between two Strings is a TypeError, and nil =~ re is nil (the generic Object#=~ was removed in Ruby
3.2, so other objects raise NoMethodError). String#match and String#match? convert a String argument to a Regexp, but
sub, gsub, split, scan, index and friends take a String argument literally.
# frozen_string_literal: true
line = "2026-10-18 lend 978-0-13-468599-1 by ana"
re = /(\d{4})-(\d\d)-(\d\d)/
# match returns MatchData or nil; =~ returns an index or nil; match? returns a boolean.
re.match(line) # => #<MatchData "2026-10-18" 1:"2026" 2:"10" 3:"18">
re =~ line # => 0
line =~ /by/ # => 34
line.match?(/lend/) # => true
re.match("no date here") # => nil
[/lend/ === line] # => [true]
/lend/.match(line, 12) # => nil
line.match(/\d+/, 5)[0] # => "10"
# match and =~ set the last-match state; match? does not.
line.match(/(?<verb>lend|return)/)
Regexp.last_match(:verb) # => "lend"
Regexp.last_match.pre_match.length # => 11
Regexp.last_match.post_match.length # => 25
"abc".match?(/b/)
Regexp.last_match(0) # => "lend"
"abc".match(/z/)
Regexp.last_match # => nil
Because match? allocates no MatchData and does not touch the globals, prefer it when you only need a boolean (see
Performance). Prefer match or =~ when you need the captures, and never rely on a later $1 after a
match?.
# frozen_string_literal: true
# String#match and match? convert a String argument to a Regexp; sub, gsub, split, scan, index do not.
"a.c".match?("a.c") # => true
"abc".match?("a.c") # => true
"abc".sub("a.c", "X") # => "abc"
"a.c".sub("a.c", "X") # => "X"
"abc".scan("b") # => ["b"]
# =~ needs a Regexp on one side; two Strings raise TypeError, and nil =~ is nil.
begin
"abc" =~ "b"
rescue TypeError => e
e.message # => "type mismatch: String given"
end
(nil =~ /a/) # => nil
# match yields the MatchData to a block when it matched.
"order 66".match(/\d+/) { |m| m[0].to_i * 2 } # => 132
"no digits".match(/\d+/) { |m| m[0] } # => nil
# A Regexp embedded in another keeps its own flags through to_s.
word = /ruby/i
combined = /\A#{word} on rails\z/
combined.inspect # => "/\\A(?i-mx:ruby) on rails\\z/"
combined.match?("RUBY on rails") # => true
combined.match?("RUBY ON RAILS") # => false
# Comparison: equal when source and options are equal; sources that merely behave alike differ.
plain = Regexp.new("a")
same = [plain == /a/, plain.eql?(/a/), plain.hash == /a/.hash]
same.all? # => true
[plain == /a/i, Regexp.new("[ab]") == /a|b/] # => [false, false]
# Regexp#=== makes a regexp usable in case/when and returns false for non-strings.
def classify(value)
case value
when /\A\d+\z/ then :digits
when /\A[[:alpha:]]+\z/ then :letters
else :other
end
end
[classify("42"), classify("abc"), classify(42), classify(:abc)] # => [:digits, :letters, :other, :letters]
Source: String#match, Regexp#=== and Regexp#eql?
The last match: $~, $1 and Regexp.last_match
Every successful match, =~, ===, scan, gsub, sub, [], slice, index and the other methods listed in
the next sections stores a MatchData in the special variable $~, and a failed match stores
nil. The other match variables are views of it:
| Variable | Value |
|---|---|
|
the |
|
the matched text ( |
|
the text before the match ( |
|
the text after the match ( |
|
the last group that matched ( |
|
the numbered groups; unmatched ones are |
The state is stored in the stack frame of the method that ran the match, not in a true global, and it is also local to the
thread. Two consequences follow. A match inside a called method never overwrites the caller’s $~, so a helper cannot
break your $1; and a block shares the state of the method that wrote it, so a gsub block can call
Regexp.last_match and see the match that is being replaced. A new thread starts with no match.
# frozen_string_literal: true
# The last match ($~, $1, ...) is local to the method and thread that matched.
def parse_year(text)
text =~ /(\d{4})/
Regexp.last_match(1)
end
"id 7" =~ /(\d)/
parse_year("year 2026") # => "2026"
Regexp.last_match(1) # => "7"
# A block shares the match state of the method that wrote it.
"a1b2".gsub(/\d/) { Regexp.last_match(0).to_i.succ.to_s } # => "a2b3"
Regexp.last_match(0) # => "2"
# Another thread starts with its own, empty match state.
"zzz" =~ /z/
Thread.new { Regexp.last_match }.value # => nil
Regexp.last_match(0) # => "z"
The numbered variables are read-only views of $~, but $~ itself can be assigned ($~ = nil clears it). Their
shortcuts have cryptic names, so the style guide and RuboCop prefer Regexp.last_match (cop Style/PerlBackrefs flags $1
and $&) and the long English aliases from require "English" (cop Style/SpecialGlobalVars flags $~ and $'). The
example below keeps the short names to show what they are, and says so with an explicit rubocop:disable pair; code you
write for a team should call Regexp.last_match.
# frozen_string_literal: true
require "English"
# The Perl-style globals. RuboCop prefers Regexp.last_match (Style/PerlBackrefs)
# and the English names (Style/SpecialGlobalVars), so this illustration opts out.
# rubocop:disable Style/PerlBackrefs, Style/SpecialGlobalVars
/s(\w{2}).*(c)/.match("haystack")
[$~, $&, $`, $', $+] # => [#<MatchData "stac" 1:"ta" 2:"c">, "stac", "hay", "k", "c"]
[$1, $2] # => ["ta", "c"]
# The English library gives them readable names.
[$LAST_MATCH_INFO.equal?($~), $MATCH, $PREMATCH, $POSTMATCH, $LAST_PAREN_MATCH] # => [true, "stac", "hay", "k", "c"]
# rubocop:enable Style/PerlBackrefs, Style/SpecialGlobalVars
Since Ruby 3.0, Enumerable#grep and grep_v with a Regexp and no block no longer change Regexp.last_match. With a block they
still set it for each element, as the grep example later shows.
The syntax of a pattern
Ruby’s syntax is the Onigmo syntax in its Ruby flavour. A pattern is a sequence of atoms (literal characters, classes, groups,
escapes), each optionally followed by a quantifier, combined with alternation. Everything except the metacharacters
. ? - + * ^ \ | $ ( ) [ ] { } matches itself. A literal regexp behaves like a double-quoted string for
interpolation and escapes such as \n, \t, é and \x41, but some backslash sequences have a regexp meaning
instead of the string one: in a pattern \s is whitespace, not a space, and \b is a word boundary, not a backspace
(inside brackets [\b] it is the backspace). Because the pattern is parsed first by the Ruby lexer, a single-quoted string
passed to Regexp.new needs the backslashes to survive: Regexp.new('\d+') and Regexp.new("\\d+") are the same pattern.
Anchors
An anchor matches a position, not a character. Ruby differs from many languages here: ^ and $ always mean the start and
end of a line, never of the string, and no option changes that.
| Anchor | Matches |
|---|---|
|
the start and the end of any line (just after, or just before, a newline) |
|
the start of the string |
|
the very end of the string |
|
the end of the string, or just before one final newline |
|
a word boundary (between |
|
where the previous match ended (in |
|
resets the start of the reported match: everything matched before it is not included |
Use \A...\z to validate a whole string. Using ^...$ for validation is a classic security mistake: the pattern matches any
single line of a multi-line value, so "978-0-13-468599-1\nrm -rf /" passes a ^...$ check for an ISBN. Brakeman reports this mistake in Rails
validations and the Ruby style guide warns against it; the fix is always \A and \z (\Z still
allows one trailing newline, which is what gets leaves behind).
# frozen_string_literal: true
# ^ and $ match at every line boundary; \A and \z match the whole string.
text = "978-0-13-468599-1\nrm -rf /"
text.match?(/^\d{3}-\d-\d{2}-\d{6}-\d$/) # => true
text.match?(/\A\d{3}-\d-\d{2}-\d{6}-\d\z/) # => false
"978-0-13-468599-1".match?(/\A\d{3}-\d-\d{2}-\d{6}-\d\z/) # => true
# \Z also matches before one final newline; \z does not.
"abc\n".match?(/abc\Z/) # => true
"abc\n".match?(/abc\z/) # => false
"abc\n\n".match?(/abc\Z/) # => false
# Word boundaries.
"scarf in the scan".scan(/\bsca\w*/) # => ["scarf", "scan"]
"scarf".match?(/\Bcar/) # => true
"a-b".gsub(/\b/, "|") # => "|a|-|b|"
# \G anchors a scan to the end of the previous match.
" a b c".gsub(/ /, "_") # => "____a_b_c"
" a b c".gsub(/\G /, "_") # => "____a b c"
# The offset argument moves where \G (and the search) starts.
"hello, world".match(/,/, 3) # => #<MatchData ",">
"hello, world".match(/\G,/, 3) # => nil
# \K drops everything matched so far from the result.
"price: 42".slice(/price: \K\d+/) # => "42"
"price: 42".sub(/price: \K\d+/, "99") # => "price: 99"
Character classes
A character class in square brackets matches one character out of a set. Ranges use a hyphen, a leading ^ negates the set,
classes nest ([a-z[0-9]]), and && intersects two sets ([a-w&&[^c-g]]). Inside a class most metacharacters are literal, but
\, ], ^ (first), - (between two characters) and [ still need care.
| Shorthand | Matches |
|---|---|
|
any character except a newline (any character including newline with the |
|
an ASCII word character |
|
an ASCII digit |
|
a hexadecimal digit |
|
ASCII whitespace |
|
a line break: |
|
an extended grapheme cluster (what a reader would call one character) |
The shorthand classes are ASCII only, even on UTF-8 strings. "café" =~ /\w+$/ stops before the é. For Unicode-aware
matching use the POSIX bracket expressions ([[:alpha:]], [[:digit:]], [[:alnum:]], [[:upper:]], [[:lower:]],
[[:space:]], [[:punct:]], [[:blank:]], [[:cntrl:]], [[:graph:]], [[:print:]], [[:xdigit:]], plus the
Ruby additions [[:word:]] and [[:ascii:]]) or the property syntax \p{...} described in
Unicode properties.
# frozen_string_literal: true
# Bracket expressions, ranges, negation and intersection.
"Bird Bard Byrd".scan(/B[aeiou]rd/) # => ["Bird", "Bard"]
"a1-b2".scan(/[a-z0-9]/) # => ["a", "1", "b", "2"]
"a1-b2".scan(/[^a-z0-9]/) # => ["-"]
"a-b".scan(/[a\-z]/) # => ["a", "-"]
"abcdefghijklmnopqrstuvwxyz".scan(/[a-w&&[^c-g]]/).join # => "abhijklmnopqrstuvw"
# Shorthand classes are ASCII only; POSIX brackets and \p{} are Unicode aware.
word = "café_42"
word.scan(/\w+/) # => ["caf", "_42"]
word.scan(/[[:alnum:]]+/) # => ["café", "42"]
word.scan(/\p{Alpha}+/) # => ["café"]
word.scan(/\p{^Alpha}+/) # => ["_42"]
word.scan(/\P{Alpha}+/) # => ["_42"]
"٣٤".match?(/\d/) # => false
"٣٤".match?(/[[:digit:]]/) # => true
"٣٤".match?(/\p{Nd}/) # => true
# \h is a hex digit (not the same as \H), \R is any line break, \s is ASCII whitespace.
"ff 0g".scan(/\h+/) # => ["ff", "0"]
"a\r\nb\nc".split(/\R/) # => ["a", "b", "c"]
"a b".match?(/\s/) # => false
"a b".match?(/[[:space:]]/) # => true
# Unicode categories, scripts and the dot.
"Ana Zoë 東京".scan(/\p{Lu}/) # => ["A", "Z"]
"Ana Zoë 東京".scan(/\p{Han}+/) # => ["東京"]
"Ana Zoë 東京".scan(/\p{Latin}+/) # => ["Ana", "Zoë"]
"line\nbreak".match?(/line.break/) # => false
"line\nbreak".match?(/line.break/m) # => true
Quantifiers
A quantifier follows an atom and says how many times it may repeat.
| Quantifier | Meaning |
|---|---|
|
zero or more, one or more, zero or one |
|
exactly n times |
|
at least n, at most m, between n and m |
|
lazy (minimal): as few as possible |
|
possessive: as many as possible and never give back (there is no possessive form of |
Greedy quantifiers take as much as they can and then give characters back until the rest of the pattern matches (backtracking);
lazy ones take as little as they can and grow; possessive ones and atomic groups (?>...) never give back, which can make a
match fail where a greedy one succeeds, and makes failure fast. A quantifier applies to the single preceding atom, so
repeating several characters needs a group: (?:ab)+ is repeated "ab", ab+ is "a" followed by repeated "b".
# frozen_string_literal: true
# Counted repetition.
"aaaa".match(/a{2}/)[0] # => "aa"
"aaaa".match(/a{2,}/)[0] # => "aaaa"
"aaaa".match(/a{,3}/)[0] # => "aaa"
"aaaa".match(/a{1,2}/)[0] # => "aa"
# Greedy takes as much as possible, lazy as little as possible.
html = "<b>bold</b> and <i>italic</i>"
html.match(/<.+>/)[0] # => "<b>bold</b> and <i>italic</i>"
html.match(/<.+?>/)[0] # => "<b>"
html.scan(/<(\w+)>/).flatten # => ["b", "i"]
# Possessive quantifiers and atomic groups never give characters back.
'"Quote"'.match(/".*"/) # => #<MatchData "\"Quote\"">
'"Quote"'.match(/".*+"/) # => nil
'"Quote"'.match(/"(?>.*)"/) # => nil
'"Quote"'.match(/"[^"]*+"/) # => #<MatchData "\"Quote\"">
# {n,m} has no possessive form; the + after it is a separate quantifier.
"aaa".match(/a{1,2}+/)[0] # => "aaa"
# Quantifiers apply to the last atom, so group to repeat several characters.
"abab".match(/\A(?:ab)+\z/).to_a # => ["abab"]
"abab".match(/\Aab+\z/) # => nil
Groups, captures and backreferences
Parentheses group atoms and capture the text they matched. Captures are numbered left to right by their opening parenthesis,
counting from 1; group 0 is the whole match. A group that did not take part in the match (an optional one, or a branch
of an alternation) is nil. The variants are:
| Syntax | Meaning |
|---|---|
|
capturing group |
|
non-capturing group |
|
named capturing group |
|
atomic group |
|
switch modes for the rest of the group, or for the subexpression |
|
comment |
|
backreference: the text the group captured |
|
subexpression call: run the group’s pattern again |
|
the whole match, usable only in a replacement string ( |
Named groups and backreferences
Once a pattern contains a named group, plain parentheses stop capturing (they behave as (?:...)), so you cannot mix named and
numbered captures; RuboCop’s Lint/MixedRegexpCaptureTypes flags the attempt. MatchData#[] takes a Symbol or a String
name. When a regexp literal with named groups stands on the left of =~, Ruby assigns each capture to a local variable of
that name (and nil if the match failed). That happens only for a literal on the left side: not with match, not with the
operands swapped, and not when the pattern is built with interpolation. A name may appear twice (in different alternatives);
the reader then sees the one that matched. Names must start with a letter or underscore; a lowercase first letter is
conventional because the name becomes a local variable.
# frozen_string_literal: true
# Capturing groups are numbered by their opening parenthesis.
date = /(\d{4})-(\d\d)-(\d\d)/.match("due 2026-10-18")
date.captures # => ["2026", "10", "18"]
date.to_a # => ["2026-10-18", "2026", "10", "18"]
# An optional group that did not take part is nil.
/July 4(th)?/.match("July 4").to_a # => ["July 4", nil]
# (?: ) groups without capturing.
/(?:\d{4})-(\d\d)-(\d\d)/.match("2026-10-18").captures # => ["10", "18"]
# Named groups: (?<name>) or (?'name'). Once a name exists, plain groups stop capturing.
price = /\$(?<dollars>\d+)\.(?'cents'\d+)/.match("$3.67") # => #<MatchData "$3.67" dollars:"3" cents:"67">
price[:dollars] # => "3"
price["cents"] # => "67"
price[2] # => "67"
Regexp.new("\\$(?<dollars>\\d+)\\.(\\d+)").match("$3.67") # => #<MatchData "$3.67" dollars:"3">
# A regexp literal on the left of =~ creates local variables from named groups.
if /(?<year>\d{4})-(?<month>\d\d)/ =~ "released 2025-12"
[year, month] # => ["2025", "12"]
end
# Backreferences: \1 .. \9, \k<name>, and \0 in a replacement.
/(?<q>["']).*?\k<q>/.match(%(say "hi" and 'bye')) # => #<MatchData "\"hi\"" q:"\"">
/[csh](..) [csh]\1 in/.match("The cat sat in the hat")[0] # => "cat sat in"
"The cat sat".gsub(/[cs]at/, '[\0]') # => "The [cat] [sat]"
"Ana Ruiz".sub(/(?<first>\w+) (?<last>\w+)/, '\k<last>, \k<first>') # => "Ruiz, Ana"
# Interpolation does not create local variables.
pattern = "(?<n>\\d+)"
/#{pattern}/ =~ "42"
[defined?(n)] # => [nil]
# Regexp#names and #named_captures describe the groups; a repeated name lists every index.
/(?<a>.)(?<b>.)/.named_captures # => {"a" => [1], "b" => [2]}
/(?<d>\d)x|y(?<d>\d\d)/.named_captures # => {"d" => [1, 2]}
/(?<d>\d)x|y(?<d>\d\d)/.match("y12")[:d] # => "12"
In replacement strings of sub and gsub, the backslash sequences are \1 to \9, \0 and \& (the whole match), \`
(before), \' (after), \k<name> and \\ (a literal backslash). A replacement written in double quotes needs every backslash
doubled, so single quotes ('\2/\1') are clearer, and a block avoids the escapes entirely.
Lookaround, absence, conditionals and recursion
Lookaround assertions check text around the current position without consuming it. They take the groups (?=...) (lookahead),
(?!...) (negative lookahead), (?<=...) (lookbehind) and (?<!...) (negative lookbehind). A lookbehind must be of fixed
width, though its top-level alternatives may differ in length ((?<=a|bc) is accepted, (?<=a+) is a RegexpError).
Ruby adds three constructs found in few other engines:
-
the absence operator
(?~pattern)matches any string (including the empty one) that does not containpattern, which is the clean way to write "everything up to the first closing delimiter"; -
conditionals
(?(1)yes|no)or(?(<name>)yes|no)choose a branch depending on whether a group captured; -
subexpression calls
\g<name>run a group’s pattern again at the current position, which makes recursive patterns such as balanced parentheses possible.
# frozen_string_literal: true
# Lookahead and lookbehind are zero-width.
"price 10 EUR, 20 USD".scan(/\d+(?= EUR)/) # => ["10"]
"price 10 EUR, 20 USD".scan(/\d+(?! EUR)\b/) # => ["20"]
"EUR10 USD20".scan(/(?<=EUR)\d+/) # => ["10"]
"EUR10 USD20".scan(/(?<!EUR)\b\d+/) # => []
"EUR10 USD20".scan(/(?<!EUR)\d+/) # => ["0", "20"]
# Lookbehind must be fixed width, though top-level alternatives may differ.
"a1 bc2".scan(/(?<=a|bc)\d/) # => ["1", "2"]
begin
Regexp.new("(?<=a+)b")
rescue RegexpError => e
e.message # => "invalid pattern in look-behind: /(?<=a+)b/"
end
# Insert thousands separators with a lookahead that counts remaining digits.
"1234567".gsub(/(?<=\d)(?=(?:\d{3})+\z)/, ",") # => "1,234,567"
# Absence operator (?~...): anything that does not contain the subexpression.
"/* a */ b /* c */".match(%r{/\*(?~\*/)\*/})[0] # => "/* a */"
# Conditionals: (?(1)yes|no) tests whether group 1 took part.
quoted = /\A(")?\w+(?(1)")\z/
['"x"', "x", '"x x"'].map { |s| quoted.match?(s) } # => [true, true, false]
# Subexpression calls: \g<name> re-runs a group, which allows recursion.
balanced = /\A(?<paren>\((?:[^()]|\g<paren>)*\))\z/
["(a(b)c)", "(a(b c)", "()"].map { |s| balanced.match?(s) } # => [true, false, true]
Modes: i, m, x and o
Four letters after the closing slash (or in the options argument of Regexp.new) change how a pattern is read.
| Letter | Constant | Effect |
|---|---|---|
|
|
ignore case, including non-ASCII letters (Unicode case folding) |
|
|
extended mode: whitespace in the pattern is ignored and |
|
|
the dot matches a newline (what other languages call dot-all) |
|
none |
interpolate |
|
|
select the encoding, see Encodings |
The m option is the one that surprises people: in Ruby it affects only the dot, never ^ or $, which are already
line anchors. In extended mode a space or # that you want to match must be escaped (\ , \#) or placed in a bracket
expression ([ ]). Extended mode with named groups and comments is the way to keep a long pattern readable. The mode letters can also be
turned on and off inside the pattern for a part of it, as (?i)te(?-i)st and t(?i:e)st show.
# frozen_string_literal: true
# i: case-insensitive. Ruby's m is "dot matches newline", not "multi-line anchors".
/ruby/i.match?("RUBY") # => true
/a.c/.match?("a\nc") # => false
/a.c/m.match?("a\nc") # => true
/^b/m.match?("a\nb") # => true
/\Ab/m.match?("a\nb") # => false
# x: whitespace and # comments are ignored, so a pattern can be documented.
ISBN13 = /
\A
(?<prefix>97[89]) # GS1 prefix
-?(?<group>\d{1,5}) # registration group
-?(?<publisher>\d{1,7})
-?(?<title>\d{1,6})
-?(?<check>\d) # check digit
\z
/x
parts = ISBN13.match("978-0-13-468599-1").named_captures
parts.values_at("prefix", "publisher", "check") # => ["978", "13", "1"]
/a b/x.match?("ab") # => true
/a\ b/x.match?("a b") # => true
/a[ ]b/x.match?("a b") # => true
# Modes can be switched inside a pattern, for the rest of the group or for a sub-group.
/(?i)te(?-i)st/.match?("TEst") # => true
/(?i)te(?-i)st/.match?("TEST") # => false
/t(?i:e)st/.match?("tEst") # => true
/(?m:a.b)c.d/.match?("a\nbc\nd") # => false
# Flags as integers and as a string (Ruby 3.2); Regexp#options reports the integer.
Regexp.new("a", Regexp::IGNORECASE | Regexp::MULTILINE).options # => 5
Regexp.new("a", "ix").inspect # => "/a/ix"
[Regexp::IGNORECASE, Regexp::EXTENDED, Regexp::MULTILINE] # => [1, 2, 4]
Regexp.new("a", true).inspect # => "/a/i"
# The o flag interpolates once; the value at the first evaluation sticks.
def build(word) = /#{word}/o
[build("cat").source, build("dog").source] # => ["cat", "cat"]
Source: Ruby 4.0 documentation: Modes, Extended Mode and Regexp.new
Unicode and encodings
Unicode properties
\p{Name} matches a character that has a Unicode property, \P{Name} and \p{^Name} match one that does not. Names are
case-insensitive, and a space, hyphen or underscore are equivalent. Ruby 4.0 uses Unicode 17.0 and Emoji 17.0 (Ruby 3.2
used 15.0). The families are:
| Family | Examples |
|---|---|
General categories (long or short names) |
|
Scripts |
|
Blocks |
|
POSIX-like names |
|
Other properties |
|
An unknown property name is an error at parse time (invalid character property name). The complete list is on the Ruby
documentation’s unicode properties page. Property syntax is the portable way to say "a letter in any language" and "a decimal
digit in any script", which \w and \d deliberately do not.
Encodings
A regexp has an encoding. A pattern of only US-ASCII characters is US-ASCII and can match a string in any ASCII-compatible
encoding; one that contains a non-ASCII character takes the source encoding (UTF-8 for a standard file) and is then fixed:
it can match only ASCII-only strings or strings of its own encoding, otherwise Encoding::CompatibilityError is raised. The
letters n (no encoding: binary when the pattern has bytes above 127), u (UTF-8), e (EUC-JP) and s (Windows-31J) force
one, and Regexp::FIXEDENCODING does the same through Regexp.new. Matching a string with an invalid byte sequence raises
ArgumentError; String#scrub or String#valid_encoding? is the way to handle untrusted input before matching.
# frozen_string_literal: true
# Encodings of regexps.
/abc/.encoding # => #<Encoding:US-ASCII>
/é/.encoding # => #<Encoding:UTF-8>
/abc/u.encoding # => #<Encoding:UTF-8>
/abc\xff/n.encoding # => #<Encoding:BINARY (ASCII-8BIT)>
/abc/.fixed_encoding? # => false
/é/.fixed_encoding? # => true
# A fixed-encoding regexp only matches ASCII-compatible strings of its own encoding.
latin = "caf\xE9".dup.force_encoding("ISO-8859-1")
begin
/é/.match?(latin)
rescue Encoding::CompatibilityError => e
e.message # => "incompatible encoding regexp match (UTF-8 regexp with ISO-8859-1 string)"
end
# Invalid byte sequences raise ArgumentError on a match.
begin
"abc\xff".match?(/b/)
rescue ArgumentError => e
e.message # => "invalid byte sequence in UTF-8"
end
"abc\xff".scrub.match?(/b/) # => true
# Invalid patterns raise RegexpError when built from strings (and SyntaxError for literals).
begin
Regexp.new("(unclosed")
rescue RegexpError => e
e.message # => "end pattern with unmatched parenthesis: /(unclosed/"
end
begin
Regexp.new("[z-a]")
rescue RegexpError => e
e.message # => "empty range in char class: /[z-a]/"
end
begin
Regexp.new("a", "q")
rescue ArgumentError => e
e.message # => "unknown regexp option: q"
end
Methods that accept a Regexp
Besides Regexp and MatchData themselves, the reference documentation lists these core methods as taking a pattern:
| Receiver | Methods |
|---|---|
|
|
|
|
|
|
scan returns every match: an Array of strings when the pattern has no groups, an Array of capture Arrays when it has, and with
a block it yields each and returns the receiver. sub replaces the first match and gsub all of them. The replacement can
be a string with backslash references, a Hash (the match text is the key; a missing key gives an empty string), or a block that
receives the matched text and can read the captures with Regexp.last_match. split accepts a pattern as the separator;
captures are included in the result, a limit of -1 keeps trailing empty fields, and an empty pattern splits into
characters. slice/[] accept a pattern and an optional group index or name; partition/rpartition split into the text
before, the match and the text after. start_with? matches only at the start and sets the last match. Enumerable#grep takes
a pattern (anything with ===) and an optional block that maps the matches, and case/when uses === too.
# frozen_string_literal: true
log = "lend 978-0-13-468599-1 ana\nreturn 978-0-13-468599-1 ana\nlend 978-0-596-51774-8 bob"
# scan: all matches; with groups it returns arrays of captures.
log.scan(/^(\w+) (\S+) (\w+)$/).first # => ["lend", "978-0-13-468599-1", "ana"]
log.scan(/\b(?:lend|return)\b/) # => ["lend", "return", "lend"]
log.scan(/\d{3}-\d-\d{2}-\d{6}-\d/).uniq.size # => 1
lent = []
log.scan(/^lend (\S+)/) { |(isbn)| lent << isbn.delete("-") }
lent.join(",") # => "9780134685991,9780596517748"
# sub / gsub: strings, hashes and blocks as replacements.
"ana ruiz".sub(/\w+/, &:capitalize) # => "Ana ruiz"
"ana ruiz".gsub(/\b(\w)/) { Regexp.last_match(1).upcase } # => "Ana Ruiz"
"a-b_c".gsub(/[-_]/, "-" => "+", "_" => "=") # => "a+b=c"
"2026-10-18".gsub(/(\d+)-(\d+)-(\d+)/, '\3/\2/\1') # => "18/10/2026"
"2026-10-18".gsub(/(?<y>\d+)-(?<m>\d+)-(?<d>\d+)/, '\k<d>.\k<m>.\k<y>') # => "18.10.2026"
"a.b".gsub(".", "-") # => "a-b"
"a\\b".gsub("\\", "\\\\\\\\") # => "a\\\\b"
"a\\b".gsub("\\") { "\\\\" } # => "a\\\\b"
# split with a regexp; captures are kept; limit and the empty pattern.
"a, b;c d".split(/[,;\s]+/) # => ["a", "b", "c", "d"]
"a1b22c".split(/(\d+)/) # => ["a", "1", "b", "22", "c"]
"a1b".split(/(?<=\d)|(?=\d)/) # => ["a", "1", "b"]
"a, b,,".split(/,\s*/) # => ["a", "b"]
"a, b,,".split(/,\s*/, -1) # => ["a", "b", "", ""]
# Other String methods that take a regexp.
"ISBN 9780134685991".slice(/\d{13}/) # => "9780134685991"
"ISBN 9780134685991"[/(?<n>\d{13})/, "n"] # => "9780134685991"
"ISBN 9780134685991"[/ISBN (\d)/, 1] # => "9"
"key = value".partition(/\s*=\s*/) # => ["key", " = ", "value"]
"archive.tar.gz".rpartition(/\.[a-z]+\z/) # => ["archive.tar", ".gz", ""]
"hello world".index(/o/) # => 4
"hello world".rindex(/o/) # => 7
"hello".start_with?(/h.l/) # => true
:symbol.match?(/sym/) # => true
:symbol[/b.l/] # => "bol"
# Enumerable#grep, case/when and slice_before use ===.
%w[ruby go rust].grep(/\Aru/) # => ["ruby", "rust"]
%w[ruby go rust].grep_v(/\Aru/) # => ["go"]
%w[ruby go rust].grep(/\Aru(.)/) { Regexp.last_match(1) } # => ["b", "s"]
%w[ruby go rust].any?(/^g/) # => true
RuboCop nudges toward the simplest tool: Style/RedundantRegexpArgument flags a regexp where a plain string argument does the
same (split(/,/) becomes split(",")), Performance/RegexpMatch (from the rubocop-performance extension) flags =~ and
match where only a boolean is used and suggests match?, and Style/StringChars replaces split(//) with chars.
MatchData
A successful match returns a MatchData, a snapshot independent of the later last-match state. Its methods:
| Method | Returns |
|---|---|
|
a group by index, name, Symbol or range; |
|
all groups including the whole match, or only the numbered ones |
|
the group names; a Hash of name to text ( |
|
the text before and after the match |
|
character positions of a group; |
|
text and length of a group, nil-safe (3.1) |
|
the original string (frozen), the pattern, the number of groups plus one |
|
the captures as Array and as symbol-keyed Hash, for |
|
equal when the same pattern matched the same string at the same place |
Positions are in characters, and for multibyte strings differ from byte offsets, which is why the byte-based methods exist
(they are what String#byteslice expects). MatchData#string is a frozen copy, so mutating the source string after the match
does not change the match.
# frozen_string_literal: true
md = /(?<year>\d{4})-(?<month>\d\d)-(?<day>\d\d)/.match("due 2026-10-18, extended")
md.to_a # => ["2026-10-18", "2026", "10", "18"]
md.captures # => ["2026", "10", "18"]
md.names # => ["year", "month", "day"]
md.named_captures # => {"year" => "2026", "month" => "10", "day" => "18"}
md.named_captures(symbolize_names: true) # => {year: "2026", month: "10", day: "18"}
md[0] # => "2026-10-18"
md[:month] # => "10"
md["day"] # => "18"
md[1..2] # => ["2026", "10"]
md.values_at(:year, :day) # => ["2026", "18"]
md.size # => 4
md.string # => "due 2026-10-18, extended"
md.regexp.source # => "(?<year>\\d{4})-(?<month>\\d\\d)-(?<day>\\d\\d)"
md.pre_match # => "due "
md.post_match # => ", extended"
# Positions: character offsets, byte offsets (3.2), and byte begin/end (3.4).
md.begin(0) # => 4
md.end(:year) # => 8
md.offset(:month) # => [9, 11]
md.byteoffset(:day) # => [12, 14]
md.bytebegin(0) # => 4
md.byteend(0) # => 14
# match and match_length (3.1) return the group text or its length.
md.match(:year) # => "2026"
md.match_length(:year) # => 4
md.match_length(:day) # => 2
# Offsets count characters, not bytes, in multibyte strings.
accent = /é/.match("café au lait")
[accent.begin(0), accent.byteoffset(0)] # => [3, [3, 5]]
# MatchData supports pattern matching (3.2): deconstruct gives the captures, deconstruct_keys the named ones.
def period(match)
case match
in { year:, month: } then "#{month}/#{year}"
end
end
def parts(match)
case match
in [year, month, day] then [day, month, year]
end
end
period(md) # => "10/2026"
parts(md) # => ["18", "10", "2026"]
md in { year: "2026" } # => true
Performance
A regexp match is fast, but three habits make a visible difference in hot paths:
-
use
match?when you need a boolean, andstart_with?orinclude?with a String when you only need a prefix or a substring: they skip theMatchDataand the global bookkeeping; -
build a pattern once: a literal without interpolation is compiled once, but
/\A#{prefix}/andRegexp.newrun the compiler on every evaluation, so hoist such patterns into a constant or a local variable (orRegexp.unionthe words once); -
anchor patterns and make the failing case fail fast: an unanchored pattern tries every start position, and a leading
.*forces backtracking at each one.
# frozen_string_literal: true
ISBN_SHAPE = /\A\d{3}-\d-\d{2}-\d{6}-\d\z/
SAMPLE = "978-0-13-468599-1"
prefix = "978"
fixed = /\A#{prefix}/
# Objects allocated while the block runs.
def allocations
before = GC.stat(:total_allocated_objects)
yield
GC.stat(:total_allocated_objects) - before
end
counts = {
match?: allocations { 1000.times { ISBN_SHAPE.match?(SAMPLE) } },
match: allocations { 1000.times { ISBN_SHAPE.match(SAMPLE) } },
interpolated: allocations { 1000.times { /\A#{prefix}/ } },
prebuilt: allocations { 1000.times { fixed.match?(SAMPLE) } }
}
# match? allocates nothing per call; match builds a MatchData each time;
# a literal with interpolation builds a new Regexp (and String) on every evaluation.
counts.transform_values { |n| n >= 1000 } # => {match?: false, match: true, interpolated: true, prebuilt: false}
Source: Ruby 4.0 documentation: Optimization and GC.stat
Safety: backtracking, ReDoS and timeouts
Until Ruby 3.1 the matcher was a plain backtracking engine, so a pattern with nested repetition such as (a+)+$ could take
exponential time on a hostile input, and a service that matched user text against such a pattern could be taken down by one
request (regular expression denial of service, ReDoS). Ruby 3.2 added two defences, and 3.3 widened the first.
-
Memoization. The matcher caches the states it has already explored and does not retry them, so matching time grows linearly with the input for most patterns (Ruby 3.2).
Regexp.linear_time?reports whether a pattern qualifies. Patterns with backreferences and subexpression calls do not; lookarounds and atomic groups qualify since Ruby 3.3. The method is a statement about the interpreter build, not about the pattern text, and it explicitly gives no compatibility promise. The cache uses memory proportional to the input, so an untrusted source can still cost memory. -
Timeouts.
Regexp.timeout = secondssets a global limit andRegexp.new(source, timeout: seconds)a per-pattern one (Ruby 3.2); an instance timeout overrides the class one, andnilmeans no limit. A match that exceeds it raisesRegexp::TimeoutError, a subclass ofRegexpError. Set a class-wide timeout at program start in any service that handles untrusted text, and also never build a pattern from untrusted text withoutRegexp.escape.
# frozen_string_literal: true
# Ruby 3.2 memoizes backtracking, so most patterns match in time linear in the input.
Regexp.linear_time?(/\A(\w+)\z/) # => true
Regexp.linear_time?("(a+)+$") # => true
# Lookarounds and atomic groups joined the optimization in Ruby 3.3.
Regexp.linear_time?(/(?<=a)b/) # => true
Regexp.linear_time?(/(?>a+)b/) # => true
# Backreferences and subexpression calls still disable it.
Regexp.linear_time?(/\A(a+)+\1\z/) # => false
Regexp.linear_time?(/(?<n>a)\g<n>/) # => false
# A per-regexp timeout (seconds, Float) stops a catastrophic match.
guarded = Regexp.new("\\A(a+)+\\1\\z", timeout: 0.05)
guarded.timeout # => 0.05
begin
guarded.match?("#{'a' * 30}b")
rescue Regexp::TimeoutError => e
[e.class, e.class.superclass, e.message] # => [Regexp::TimeoutError, RegexpError, "regexp match timeout"]
end
# The class-wide default applies to every regexp whose own timeout is nil.
Regexp.timeout # => nil
Regexp.timeout = 1.0
Regexp.timeout # => 1.0
/x/.timeout # => nil
Regexp.timeout = nil
The Timeout module’s Timeout.timeout is not a substitute: it interrupts the thread with an asynchronous exception, which has
the caveats described in Security and
Threads and the GVL, while Regexp.timeout stops the match inside the
engine.
Warnings and errors from patterns
With ruby -w the parser warns about patterns that are valid but suspicious: a duplicate range inside a character class
([aa], [a-ca-c]) and a redundant nested repeat operator (a**). Invalid patterns are errors: RegexpError for
Regexp.new, SyntaxError for a literal, naming the problem and the pattern. Typical messages are
end pattern with unmatched parenthesis, empty range in char class, invalid pattern in look-behind,
unmatched range specifier in char-class (a class shorthand used as a range end, as in [\d-z]), and
invalid backref number/name.
# frozen_string_literal: true
# Run with ruby -w: both warnings are printed to standard error.
Regexp.new("[aa]")
Regexp.new("a**")
begin
Regexp.new("[\\d-z]")
rescue RegexpError => e
puts e.message
end
Running it with ruby -w w2.rb prints, on standard error and standard output:
w2.rb:4: warning: character class has duplicated range: /[aa]/
w2.rb:5: warning: regular expression has redundant nested repeat operator '*': /a**/
unmatched range specifier in char-class: /[\d-z]/
Source: Ruby 4.0 documentation: Character Classes and Regexp
Using regular expressions in the shelf library
The following file parses the lines of a loan log with an extended-mode pattern and named groups, wraps the result in a Data
value object, builds a safe search from words typed by a user, and shows that any object with === can take part in
case/when like a regexp does.
# frozen_string_literal: true
module Shelf
# Parses lines of the loan log, for example:
# 2026-10-18T09:30:00Z lend 978-0-13-468599-1 ana
class LogLine
PATTERN = /
\A
(?<at>\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ)\s+ # ISO 8601 timestamp
(?<action>lend|return)\s+
(?<isbn>[\d-]{13,17})\s+ # digits and hyphens
(?<member>[[:alpha:]][[:alnum:]_.-]*) # a member name
\z
/x
Entry = Data.define(:at, :action, :isbn, :member)
# A callable with === works in case/when and grep, like a regexp does.
IsbnLike = ->(text) { text.to_s.delete("-").match?(/\A\d{13}\z/) }
def self.parse(line)
match = PATTERN.match(line.strip) or raise ArgumentError, "unparsable log line: #{line.inspect}"
Entry.new(**match.named_captures(symbolize_names: true))
end
end
end
entry = Shelf::LogLine.parse("2026-10-18T09:30:00Z lend 978-0-13-468599-1 ana\n")
entry.action # => "lend"
entry.member # => "ana"
entry.to_h.values_at(:at, :isbn) # => ["2026-10-18T09:30:00Z", "978-0-13-468599-1"]
begin
Shelf::LogLine.parse("garbage")
rescue ArgumentError => e
e.message # => "unparsable log line: \"garbage\""
end
# Searching a catalogue safely: escape the user's words, then join them.
words = ["c++", "(2nd)"]
Regexp.union(words).source # => "c\\+\\+|\\(2nd\\)"
query = Regexp.union(words.map { |w| /#{Regexp.escape(w)}/i })
titles = ["Learning C++ (2nd ed.)", "Learning Ruby", "Effective C++"]
titles.grep(query) # => ["Learning C++ (2nd ed.)", "Effective C++"]
def kind(text)
case text
when Shelf::LogLine::IsbnLike then "looks like an ISBN"
else "something else"
end
end
kind("978-0-13-468599-1") # => "looks like an ISBN"
kind("Learning Ruby") # => "something else"
Source: MatchData#named_captures, Regexp.union and Data
Pitfalls checklist
| Pitfall | Do this instead |
|---|---|
validating with |
anchor with |
trusting |
use |
thinking |
|
using |
|
using |
capture into a variable immediately, or use |
interpolating untrusted text into a pattern |
|
|
build once with a constant or variable; |
unanchored or nested-quantifier patterns on untrusted input |
anchor, simplify, and set |
mixing named and numbered groups |
use only named groups, or only numbered ones |
expecting local variables from |
only a literal on the left of |
|
|
double-quoted replacement |
the string escape turns it into a control character; write |
|
in a double-quoted string |
|
a failed match is |
Where older books and code are outdated
Books and articles on Ruby regular expressions often predate the current engine, and the two books consulted for this section give the subject only a few pages. Treat the following as the current position:
| Older statement | Current Ruby (4.0) |
|---|---|
Regular expressions can always hang on pathological input and nothing can be done |
Ruby 3.2 memoizes most patterns into linear time, adds |
|
pass |
|
removed in Ruby 3.2; only String, Symbol and Regexp (and |
|
literals are frozen since Ruby 3.0 |
|
they are local to the method frame and thread |
|
not without a block, since Ruby 3.0 |
Unicode behaviour of |
Unicode 17.0 and Emoji 17.0 in Ruby 4.0 |
|
|
|
it supports |
Version notes
| Version | Change |
|---|---|
3.0 |
Regexp literals are frozen; |
3.1 |
|
3.2 |
Cache-based linear-time matching, |
3.3 |
The linear-time cache also handles lookarounds and atomic groups; |
3.4 |
|
4.0 |
Unicode 17.0 and Emoji 17.0 for property classes. |
4.1 (preview, not released as of 2026-10-18) |
The development NEWS (the |
References
-
Ruby 4.0 documentation: Regexp, covering syntax, modes, encodings, timeouts and optimization
-
MatchData and Regexp Methods (every core method that takes a pattern)
-
Ruby 3.2.0 NEWS (linear time,
Regexp.timeout), 3.3.0 (lookaround in the cache), 3.0.0 (frozen literals) and 4.0.0 (Unicode 17) -
String, Symbol and Enumerable (the methods that accept a Regexp)
-
The Ruby Style Guide and RuboCop Style cops (
Style/PerlBackrefs,Style/SpecialGlobalVars,Style/RedundantRegexpArgument) -
Rubular, an interactive Ruby regexp editor, and Onigmo (the engine)