ZetCode

Regex Literals and Regexp.new

Regular expressions in Ruby are written as literals between slashes, such as /pattern/, with optional flags after the closing slash. The Regexp.new constructor builds the same kind of object from a string at runtime. The =~ operator returns the index of the first match or nil, while match? returns a simple boolean.

literals.rb
# Regex literal and Regexp.new
re1 = /ruby/i
re2 = Regexp.new("r(u)by")

puts re1.source
puts re1.inspect
puts re2.inspect

# =~ returns the match index, or nil
puts "I love ruby" =~ re1
p("nothing here" =~ re1)

# match? returns only true or false
puts "ruby".match?(re2)
puts "rubyy".match?(/\Aruby\z/)

%w[Ruby ruby RUBY python].each do |w|
  puts "#{w}: #{w.match?(re1)}"
end

The /ruby/i literal has the i flag for case-insensitive matching. Regexp.new creates an equivalent object from a string; the parentheses in r(u)by form a capture group. =~ returns 7 for the position in the string, and nil when there is no match.

$ ./literals.rb
ruby
/ruby/i
/r(u)by/
7
nil
true
false
Ruby: true
ruby: true
RUBY: true
python: false

match? does not set the global match variables and is the cheapest choice when you only need a yes/no answer. The \A and \z anchors require the whole string to match.

MatchData and Captures

Calling match returns a MatchData object, or nil on failure. Numbered captures are accessed with m[1], and named captures with (?<name>...) can be read as m[:name]. This example parses a date from a line of text.

match_captures.rb
# MatchData, numbered and named captures
line = "Released on 2026-10-18 by Jan"

m = line.match(/(\d{4})-(\d{2})-(\d{2})/)
puts m[0]
puts m[1]
puts m[2]
puts m.pre_match.inspect
puts m.begin(0)

date = line.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
puts "Year: #{date[:year]}"
puts "Month: #{date[:month]}"
puts "Day: #{date[:day]}"
puts date.names.inspect
puts date.captures.inspect

if line =~ /by (\w+)/
  puts "Author: #{$1}"
  puts "Full: #{$~[0]}"
end

puts "no match".match(/\d+/).inspect

m[0] is the whole match, and m[1] to m[3] are the numbered groups. The named version exposes year, month, and day by name, and names and captures list them. Inside an if =~ block, the globals $~ (the last MatchData) and $1 (the first group) are set, and $1 is the author name here. A failed match returns nil.

$ ./match_captures.rb
2026-10-18
2026
10
"Released on "
12
Year: 2026
Month: 10
Day: 18
["year", "month", "day"]
["2026", "10", "18"]
Author: Jan
Full: by Jan
nil

Scan, sub and gsub

scan returns all matches as an array, and with groups it returns arrays of captures. sub replaces the first match and gsub replaces all of them. Replacements can be a string with back-references like \1 or \k<name>, or a block that receives the match through $1.

scan_gsub.rb
# scan, sub, gsub with blocks and back-references
text = "Prices: $12, $7 and $150"

puts text.scan(/\$\d+/).inspect
puts text.scan(/\$(\d+)/).flatten.inspect

puts text.sub(/\$(\d+)/) { "EUR#{$1.to_i * 2}" }
puts text.gsub(/\$(\d+)/) { |_| "#{$1.to_i + 1} USD" }

puts "John Smith".sub(/(\w+) (\w+)/, '\2, \1')
puts "2026-10-18".gsub(/(\d+)-(\d+)-(\d+)/, '\3/\2/\1')
puts "hello world".gsub(/o/, "0")

puts "snake_case_name".gsub(/_(\w)/) { $1.upcase }
puts "a1b22c333".gsub(/\d+/) { |num| "<#{num.length}>" }
puts "cat hat bat".gsub(/(?<first>[chb])at/, '\k<first>og')

scan returns the whole matches, or only the captured numbers when the pattern has a group. The block form of sub doubles each price, and gsub with a block adds one. In the string form, '\2, \1' swaps the first and last names, and \k<first> refers to a named group. Single quotes keep the backslashes literal.

$ ./scan_gsub.rb
["$12", "$7", "$150"]
["12", "7", "150"]
Prices: EUR24, $7 and $150
Prices: 13 USD, 8 USD and 151 USD
Smith, John
18/10/2026
hell0 w0rld
snakeCaseName
a<1>b<2>c<3>
cog hog bog

Split, Flags, Anchors and Character Classes

split accepts a regex as its separator, which lets you split on several different delimiters at once. The flags i (case-insensitive), m (dot matches newline), and x (extended, whitespace and comments ignored) modify how a pattern is interpreted. Anchors such as \A and \z and character classes such as \d and [-\s] restrict what matches.

split_flags.rb
# split with a regex and the i, m, x flags
csv = "apple,  banana;cherry ;  date"
puts csv.split(/\s*[,;]\s*/).inspect
puts "one1two22three".split(/\d+/).inspect
puts "a b\tc\n d".split(/\s+/).inspect

# i: case-insensitive
puts "Hello HELLO hello".scan(/hello/i).length

# m: dot matches newlines
block = "start\nmiddle\nend"
puts block[/start.*end/m].inspect
puts block[/start.*end/].inspect

# x: extended mode, whitespace and comments ignored
phone = /
  \A\(?(\d{3})\)?   # area code
  [-\s]?
  (\d{3})           # prefix
  -(\d{4})\z        # line number
/x
puts "(555) 123-4567".match?(phone)
puts "555-123-456".match?(phone)

The first split treats commas and semicolons, with any surrounding spaces, as separators. /hello/i matches all three spellings. Without m, . stops at the newline, so the slice returns nil; with m it spans the lines. The x flag lets the phone pattern be split across lines with comments, and \A/\z reject the 9-digit number.

$ ./split_flags.rb
["apple", "banana", "cherry", "date"]
["one", "two", "three"]
["a", "b", "c", "d"]
3
"start\nmiddle\nend"
nil
true
false

Practical: Parsing Logs and Validating Input

A common use of regular expressions is parsing structured text and validating user input. This example uses a named-capture pattern with anchors to extract fields from log lines, and a second pattern to check simple email addresses.

log_validate.rb
# Practical: parse log lines and validate input
logs = [
  "2026-10-18 10:15:02 ERROR Disk full on /dev/sda1",
  "2026-10-18 10:16:44 INFO User alice logged in",
  "garbage line without a timestamp"
]

LOG_PATTERN = /\A(?<date>\d{4}-\d{2}-\d{2}) (?<time>[\d:]{8}) (?<level>[A-Z]+) (?<msg>.+)\z/

logs.each do |line|
  if (m = LOG_PATTERN.match(line))
    puts "[#{m[:level].ljust(5)}] #{m[:date]} #{m[:msg]}"
  else
    puts "Skipped: #{line.inspect}"
  end
end

email = /\A[\w.+-]+@[\w-]+\.[a-z]{2,}\z/i
["jan@zetcode.com", "bad@", "x.y+z@mail.org"].each do |e|
  puts "#{e}: #{e.match?(email) ? 'valid' : 'invalid'}"
end

puts "error count: #{logs.count { |l| l.include?("ERROR") }}"
puts "digits only: #{"abc123".match?(/\A\d+\z/)}"

The LOG_PATTERN constant anchors the whole line and names each field, so non-matching lines fall through to the Skipped branch. The email pattern is case-insensitive via /i and requires a domain with at least two letters. The count block and the digit check show match? used in ordinary conditions.

$ ./log_validate.rb
[ERROR] 2026-10-18 Disk full on /dev/sda1
[INFO ] 2026-10-18 User alice logged in
Skipped: "garbage line without a timestamp"
jan@zetcode.com: valid
bad@: invalid
x.y+z@mail.org: valid
error count: 1
digits only: false

List all Ruby tutorials.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.