Regular Expressions
Reviewed & published by Brayan K
Ruby is a dynamic, beginner-friendly programming language with excellent built-in regular expressions — compact patterns for matching, extracting, and replacing text.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll write patterns, test matches, capture pieces of text, and transform strings with gsub and scan.
What You'll Learn in This Lesson
- Writing regex literals between slashes
- Testing matches with match? and =~
- Character classes: \d, \w, \s and anchors
- Quantifiers: +, *, ?, {n,m}
- Capturing pieces with groups
- Transforming text with gsub, sub, and scan
1️⃣ Matching: Patterns, Classes, and Anchors
A pattern lives between slashes: /world/. Test it with match? (true/false) or =~ (index or nil). The building blocks are character classes like \d (digit) and \w (word char), anchors ^/$, and quantifiers like +.
# A regex (pattern) is written between slashes: /pattern/
# The =~ operator tests for a match and returns the index, or nil.
puts ("hello world" =~ /world/) # -> 6 (match starts at index 6)
puts ("hello" =~ /xyz/).inspect # -> nil (no match)
# match? returns a simple true/false (fast, preferred for yes/no):
puts "cat".match?(/c.t/) # -> true . matches any one char
puts "dog".match?(/^d/) # -> true ^ anchors to the start
puts "dog".match?(/g$/) # -> true $ anchors to the end
# Character classes and quantifiers:
puts "a1b2".match?(/[0-9]/) # -> true [0-9] = any digit
puts "abc123".match?(/\d+/) # -> true \d = digit, + = one or more
puts " ".match?(/\s/) # -> true \s = whitespace
puts "hello".match?(/\w+/) # -> true \w = word character2️⃣ Extracting and Transforming
Wrap parts of a pattern in () to capture them, then read data[1], data[2], and so on. scan collects every match into an array; gsub replaces them all; and split can break a string on a pattern.
# Capture groups () pull pieces out of a match.
text = "Order 42 shipped on 2026-06-17"
if data = text.match(/(\d{4})-(\d{2})-(\d{2})/)
puts data[0] # -> 2026-06-17 (the whole match)
puts data[1] # -> 2026 (first group)
puts data[2] # -> 06 (second group)
end
# scan finds ALL matches and returns them in an array:
puts "cat bat rat".scan(/\w+at/).inspect # -> ["cat", "bat", "rat"]
# gsub replaces every match (sub does just the first):
puts "2026-06-17".gsub(/-/, "/") # -> 2026/06/17
puts "Hello, World".gsub(/[aeiou]/i, "*") # -> H*ll*, W*rld (i = ignore case)
# split on a pattern:
puts "a1b2c3".split(/\d/).inspect # -> ["a", "b", "c"]Your turn. Use a pattern to validate emails and extract a domain. The pattern is supplied — read the TODO note to understand each piece, then run it.
# 🎯 YOUR TURN — replace each TODO, then run with: ruby validate.rb
# A (very simplified) email checker.
def valid_email?(text)
# 👉 TODO: the pattern needs: some word chars, @, more word chars,
# a dot, and 2+ letters. \w means word char, . is a literal dot here.
text.match?(/\A\w+@\w+\.[a-z]{2,}\z/)
end
puts valid_email?("ada@example.com") # -> true
puts valid_email?("not-an-email") # -> false
puts valid_email?("bo@site.io") # -> true
# Bonus: extract the domain with a capture group
m = "ada@example.com".match(/@(\w+\.\w+)/)
puts "Domain: #{m[1]}" if m # -> Domain: example.com
# ✅ Expected output:
# true
# false
# true
# Domain: example.comMini-Challenge: Extract Phone Numbers
Use scan with a digit pattern to pull every phone number out of a sentence. Run with ruby phones.rb.
# 🎯 MINI-CHALLENGE: extract all phone numbers.
# Save as phones.rb and run with: ruby phones.rb
#
# Given text with numbers like "555-1234" mixed in:
# text = "Call 555-1234 or 555-9876 today!"
# 1. Use scan with a pattern \d{3}-\d{4} to find all phone numbers.
# 2. Print how many you found and the list itself.
#
# ✅ Example output:
# Found 2: ["555-1234", "555-9876"]
# your code hereCommon Errors (and the fix)
- Forgetting to escape special chars — A literal dot is \.; an unescaped . matches any character.
- nil from match — match returns nil on no match. Guard before reading data[1].
- Greedy quantifiers — .* grabs as much as possible. Use .*? for lazy matching.
- ^/$ vs \A/\z — ^/$ match line boundaries; for whole-string anchoring use \A/\z.
- Over-complex patterns — Email/URL validation is famously hard. Keep patterns simple and validate further in code.
Pro Tips
- 💡 The i flag: /cat/i matches case-insensitively.
- 💡 Named groups: /(?<year>\d{4})/ reads as data[:year].
- 💡 Test interactively: a tool like rubular.com shows matches live as you type.
📋 Quick Reference — Regular Expressions
| Concept | Ruby Syntax |
|---|---|
| Test match | s.match?(/pat/) |
| Digit / word / space | \d \w \s |
| One+ / optional | + ? * {2,4} |
| Capture | s.match(/(\d+)/)[1] |
| All matches | s.scan(/\w+/) |
| Replace all | s.gsub(/x/, "y") |
🎉 Lesson Complete!
- ✅ Patterns go between slashes: /pat/
- ✅ Test with match? or =~
- ✅ \d \w \s, anchors, and quantifiers build patterns
- ✅ Capture groups () extract pieces of text
- ✅ scan collects matches; gsub replaces them
Practice quiz
What does ("hello world" =~ /world/) return?
- true
- 0
- 6
- nil
Answer: 6. =~ returns the index where the match starts; "world" begins at index 6.
What does ("hello" =~ /xyz/) return?
- nil
- false
- 0
- -1
Answer: nil. =~ returns nil when there is no match.
What does the character class \d match?
- Any word character
- Whitespace
- Any letter
- Any digit
Answer: Any digit. \d matches any single digit 0-9.
What does \w match?
- Whitespace only
- A word character (letters, digits, underscore)
- Only letters
- Any character
Answer: A word character (letters, digits, underscore). \w matches a word character: letters, digits, or underscore.
Which method returns a simple true/false for a match?
- match?
- scan
- match
- =~
Answer: match?. match? returns a boolean and is the fastest yes/no check.
After m = "2026-06-17".match(/(\d{4})-(\d{2})-(\d{2})/), what is m[1]?
- "2026-06-17"
- "06"
- "2026"
- "17"
Answer: "2026". m[0] is the whole match; m[1] is the first capture group, "2026".
What does "cat bat rat".scan(/\w+at/) return?
- "cat"
- ["cat", "bat", "rat"]
- 3
- nil
Answer: ["cat", "bat", "rat"]. scan finds all matches and returns them in an array.
What does "2026-06-17".gsub(/-/, "/") return?
- "2026-06-17"
- "2026/06-17"
- nil
- "2026/06/17"
Answer: "2026/06/17". gsub replaces every match, turning all dashes into slashes.
What does "a1b2c3".split(/\d/) return?
- ["a1", "b2", "c3"]
- ["a", "b", "c"]
- ["1", "2", "3"]
- ["abc"]
Answer: ["a", "b", "c"]. split breaks the string on each digit, leaving the letters.
Which flag makes a pattern match case-insensitively?
- /pat/g
- /pat/m
- /pat/i
- /pat/x
Answer: /pat/i. The i flag, as in /cat/i, matches case-insensitively.
Continue this course
- Previous: Exception Handling
- Next: File I/O — Read from and write to files safely on disk
- Quick reference: Ruby cheat sheet
Frequently asked questions
How do I write and test a regex in Ruby?
A regular expression literal goes between slashes, like /\d+/. To test whether a string matches, use match? for a boolean ("abc123".match?(/\d/) returns true) — it's the fastest and clearest for yes/no checks. The older =~ operator returns the index of the first match or nil, which is handy in conditionals. For pulling data out, use match (returns a MatchData object with capture groups) or scan (returns an array of all matches).
What do \d, \w, \s, and the anchors mean?
These are character shortcuts: \d matches any digit, \w matches a word character (letters, digits, underscore), and \s matches whitespace. Their uppercase versions (\D, \W, \S) match the opposite. Anchors pin a match to a position: ^ and \A mean start, $ and \z mean end. Quantifiers say how many: + is one or more, * is zero or more, ? is optional, and {2,4} is between two and four. Combining these — like \A\d{4}\z for exactly four digits — is how you build precise patterns.
How do capture groups work?
Parentheses in a pattern create capture groups that remember the matched substring. After a successful match, the MatchData object lets you read them by number: data[0] is the entire match, data[1] is the first group, data[2] the second, and so on. So /(\d{4})-(\d{2})-(\d{2})/ against a date gives you the year, month, and day separately. You can also use named groups like (?<year>\d{4}) and read them with data[:year] for clearer code.
What's the difference between sub, gsub, and scan?
sub replaces only the first match of the pattern; gsub (global sub) replaces every match. Both return a new string and take either a replacement string or a block for computed replacements. scan doesn't replace anything — it finds and returns all matches as an array, which is perfect for extracting every email, number, or word that fits a pattern. As a rule: gsub to transform text, scan to collect matches, and sub when you specifically want just the first occurrence changed.