iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Regex

Ruby has first-class regex support — literal syntax (/.../), the Regexp class, dozens of methods (match, scan, gsub, =~), and named captures that integrate with hash destructuring. It’s one of Ruby’s standout features.

Literals, methods, captures, gsub, perf

EXAMPLE
# 1) Regex literals
/foo/                                                # case-sensitive
/foo/i                                                # case-insensitive
/^\s*foo\s*$/m                                      # multiline (^ $ match every line)
/(?<year>\d{4})-(?<month>\d{2})/                     # named captures
/foo|bar/
%r{https://[^/]+/}                                    # alternate delimiter — useful for URLs with /

# 2) Match
'Hello, world'.match(/(\w+), (\w+)/) do |m|
    puts m[0]    # 'Hello, world'
    puts m[1]    # 'Hello'
    puts m[2]    # 'world'
end

# Returns nil if no match
'no match'.match(/xyz/)                              # nil
'no match'.match?(/no/)                              # true; faster, no MatchData allocation

# 3) The =~ operator
if 'order-42' =~ /order-(\d+)/
    puts $1                                          # '42'  (global match var)
end

'order-42' =~ /order-(?<id>\d+)/
puts $~[:id]                                         # '42'

# 4) Named captures
result = 'date: 2024-01-15'.match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/)
puts result[:y], result[:m], result[:d]

# Destructure with deconstruct_keys (Ruby 3.0+)
case 'date: 2024-01-15'.match(/(?<y>\d{4})-(?<m>\d{2})/)
in { y:, m: }
    puts "\#{y}-\#{m}"
end

# 5) Scan — all matches
'a1 b22 c333'.scan(/[a-z](\d+)/)                     # [['1'], ['22'], ['333']]
'a1 b22 c333'.scan(/([a-z])(\d+)/)                   # [['a','1'], ['b','22'], ['c','333']]
'2024-01-15 2025-02-20'.scan(/\d{4}-\d{2}-\d{2}/)   # ['2024-01-15', '2025-02-20']

# 6) gsub — global substitute
'hello world'.gsub('o', '0')                          # 'hell0 w0rld'
'hello world'.gsub(/o/, '0')
'hello'.gsub(/[aeiou]/) { |v| v.upcase }              # 'hEllO'

# Backreferences
'hello world'.gsub(/(\w+)/) { "#{$1.length}-#{$1}" } # '5-hello 5-world'
'2024-01-15'.gsub(/(\d{4})-(\d{2})-(\d{2})/, '\3/\2/\1') # '15/01/2024'

# Named captures in substitution
'2024-01-15'.gsub(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, '\k<d>/\k<m>/\k<y>')

# 7) Split
'a,b,,c'.split(/,/)                                   # ['a','b','','c']
'a, b, c'.split(/,\s*/)                                # ['a','b','c']
'one two three'.split(/\s+/)                          # ['one','two','three']
'a1b2c3'.split(/\d/)                                   # ['a','b','c','']
'a,b,c'.split(',', 2)                                  # ['a','b,c']

# 8) Common patterns
email   = /^[^@\s]+@[^@\s]+\.[^@\s]+$/
url     = %r{^https?://[^\s/$.?#].[^\s]*$}i
hex_col = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i
slug    = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
iso_date = /^\d{4}-\d{2}-\d{2}$/
ip4     = /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/

# 9) Unicode support — \p{...}
/\p{L}+/u.match?('Привет')                            # true — Unicode letters
/\p{N}+/u.match?('一二三')                             # true — Unicode numbers
/\p{Han}+/u.match?('漢字')                             # true — Han script
/\p{Cyrillic}/u

# 10) Anchors
# ^ start of line     /^foo/m
# $ end of line       /foo$/m
# \A start of string  /\Afoo/
# \z end of string    /foo\z/
# \Z end of string (before optional newline)
# \b word boundary    /\bword\b/
# \B non-word boundary

# 11) Lookaround
'price: 99 USD'[/\d+(?= USD)/]                        # '99' — lookahead
'price: 99 USD'[/(?<=price: )\d+/]                    # '99' — lookbehind
'foo123'[/foo(?!\d{4})/]                              # 'foo' — negative lookahead

# 12) Heredoc + multi-line regex (x flag)
phone = /
    ^\+?           # optional +
    (?<cc>\d{1,3}) # country code
    [-\s]?          # optional separator
    (?<num>\d{6,12})$
/x

phone.match('+61 412345678')[:cc]                     # '61'

# 13) Performance — beware catastrophic backtracking
# Bad: nested quantifiers on overlapping alternations
slow_re = /^(a+)+$/
# 'a' * 30 + '!' takes seconds. Modern Ruby (Onigmo) is better but not immune.

# Better: avoid nested quantifiers; make alternations distinct
/(?:a)+$/                                            # equivalent + safe

# Test with timeout in untrusted input scenarios:
require 'timeout'
Timeout.timeout(0.1) { input.match(re) }

# 14) Compiled vs literal — Ruby caches both
RE = /foo/i                                            # constant; compiled once
str.match(RE)

# Don't build a fresh regex per loop iteration if it's static.

# 15) Common bugs
# • Forgot to anchor — partial match matches longer strings
# • Single-quoted heredoc vs double — escapes differ; use %r{} for clarity
# • $1 in libraries — global mutable; use named captures
# • Match data persists in $~ — explicit nil out after sensitive matches
# • match vs scan — match returns first; scan returns all
# • Case sensitivity forgotten — add 'i' flag for user-supplied terms
# • gsub passes match index but block expects string — block arg is the matched string
# • UTF-8 in patterns without 'u' flag — sometimes works, sometimes doesn't; be explicit
# • Hardcoded date regex breaking on 2030 — prefer Date.parse with rescue + regex as a guard

Why it matters

Ruby regex feels native: literal syntax, named captures, scan/gsub with blocks, and Unicode property escapes. Compile once into a constant when the pattern is static, lean on named captures and deconstruct_keys for clean destructuring, and watch nested quantifiers for catastrophic backtracking on user input.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
if 'Order #123' =~ /#(\d+)/
    puts $1                   # "123"
end
'a-b-c'.scan(/\w/)             # ['a','b','c']
Try it Yourself »

Discussion

Loading…