String#scan
scan finds every match of a pattern in the string and returns them as an array. When a block is given, it yields each match and returns the string itself.
Use scan when you need every match, not just the first one. It is a good fit for pattern matching in Ruby strings, especially when you are extracting tokens, IDs, dates, or repeated values from a larger piece of text.
Signature
str.scan(pattern) → array
str.scan(pattern) { |match| block } → self
Parameters:
pattern— a Regexp or String
Returns: Without a block, an Array of all matches. With a block, returns self after yielding each match in turn.
The return type changes based on whether a block is given. Without a block, you get an array that you can chain with other Enumerable methods like map or select. With a block, scan acts as an iterator that yields each match and returns the string itself, which lets you process matches inline without building up a potentially large intermediate collection in memory.
Basic Usage
Without a block, scan returns an array:
"the quick brown fox".scan(/\w+/)
# => ["the", "quick", "brown", "fox"]
"hello world".scan(/[aeiou]/)
# => ["e", "o", "o"]
Passing a block to scan changes both the return value and the calling pattern. Instead of building an array of matches in memory, Ruby yields each match to the block one at a time as it scans through the string from left to right. The block form returns the original string object rather than an array, which signals that the purpose is the side effect inside the block rather than the collected results. This streaming interface is especially useful for large inputs where you want to process matches incrementally without holding the entire match list in memory:
result = "hello world".scan(/[aeiou]/) { |v| v.upcase }
result # => "hello world"
The block form returns the original string rather than an array, which is useful when the goal is to perform a side effect for each match without collecting results. This is the streaming pattern: iterate through matches, do something with each one, and discard them. When you need the matches for later use, the non-block form that returns an array is the right choice.
Capturing Groups
When the pattern includes groups, each match is an array with one element per group:
"price: $100, total: $50".scan(/(\$\d+)/)
# => [["$100"], ["$50"]]
"abc123xyz456".scan(/(\d+)([a-z]+)/)
# => [["123", "abc"], ["456", "xyz"]]
When a pattern has multiple capturing groups and the string has overlapping matches, the results can look surprising. The example above shows two groups that capture in alternating order — digits then letters — and the resulting array reflects that order. Being explicit about which group captures what helps when the pattern grows complex and you need to unpack the nested arrays in the right sequence.
Without groups, each match is just the matched string:
"abc123xyz456".scan(/\d+/)
# => ["123", "456"]
"abc123xyz456".scan(/[a-z]+/)
# => ["abc", "xyz"]
The examples so far show how scan collects matches and how groups change the shape of the returned array. The next three examples apply those ideas to realistic tasks you might encounter when working with log files, user data, or text corpora. Each one combines scan with a different Ruby pattern to solve a specific problem that goes beyond just collecting matches.
Practical Examples
extracting structured data
data = "user: alice, user: bob, user: carol"
names = data.scan(/user: (\w+)/)
names # => [["alice"], ["bob"], ["carol"]]
# Flatten if you just want the names:
names.flatten
# => ["alice", "bob", "carol"]
This example returns nested arrays because the regular expression has a capturing group. Flattening is fine when there is only one group, but with several groups you may want to keep the nested shape so each match stays grouped.
parsing log lines
log = <<~LOG
2024-01-15 ERROR connection timeout
2024-01-16 WARN retry attempt 3
2024-01-17 ERROR db query failed
LOG
errors = log.scan(/^\d{4}-\d{2}-\d{2} ERROR (.+)$/)
errors.flatten
# => ["connection timeout", "db query failed"]
The log-parsing example filtered for lines matching a specific severity level and extracted the message text from each match. When you need to count or tally matches instead of just collecting them, pair scan with a counting data structure. The frequency-map pattern below uses a Hash with a default value of zero so that every word starts with a count and each subsequent occurrence increments it by one:
building a word frequency map
text = "the cat sat on the mat"
frequency = Hash.new(0)
text.scan(/\w+/).each { |word| frequency[word] += 1 }
frequency
# => {"the"=>2, "cat"=>1, "sat"=>1, "on"=>1, "mat"=>1}
This pattern combines scan with a hash counter, so it works well for small text cleanup and quick reports. For larger inputs, prefer the block form below so you do not need to build the full array of matches first.
with a block for streaming
When processing large strings, use a block to avoid building a large intermediate array:
File.read("large.log").scan(/ERROR: (.+)/) do |message|
puts "Error found: #{message.first}"
end
When the pattern contains capturing groups, Ruby passes each captured value as a separate argument to the block rather than passing the entire match as a single string. If there is only one group, the block still receives a single argument, though that argument is the captured substring rather than the full match string. With multiple groups, the arguments appear in left-to-right order matching the parentheses in the regular expression, so you can give each captured piece a descriptive name directly in the block parameter list:
"abc123def456".scan(/(.+?)(\d+)/) do |letters, digits|
puts "#{digits} digits follow #{letters}"
end
# Output:
# 123 digits follow abc
# 456 digits follow def
When the pattern contains multiple capturing groups, Ruby unpacks the captured values into separate block parameters in the order they appear. This destructuring syntax keeps the code readable because each group gets a descriptive variable name at the point where it is consumed, rather than forcing you to remember positional indices like match[0] or match[1]. The approach works especially well when each group captures a semantically different kind of value, such as a label and a numeric value, or a key paired with its corresponding data.
compared to other methods
scan vs split:
"one,two,three".split(",")
# => ["one", "two", "three"]
"one,two,three".scan(/[^,]+/)
# => ["one", "two", "three"]
# split is cleaner for simple delimiter-based splitting
The split vs scan comparison is instructive because both can tokenize a string, but they approach the problem from opposite directions. split describes what to remove between tokens, while scan describes what the tokens themselves look like. For simple delimiters like commas, split is more natural. For complex token patterns like words or identifiers, scan lets you express the token shape directly, which often reads more clearly.
scan vs match:
"abc123".match(/\d+/)
# => #<MatchData "123"> (first match only)
"abc123xyz456".scan(/\d+/)
# => ["123", "456"] - all matches
With match, you get a MatchData object that includes capture groups, offsets, and pre/post match context. That is useful when you need positional information or named captures from a single occurrence. With scan, you get every match at once in a plain array, which is far more convenient when you are collecting values across a whole document and do not care about the match metadata. Choose based on whether you need depth or breadth from your pattern.
scan vs gsub:
# gsub returns the transformed string
"hello".gsub(/[aeiou]/, "*")
# => "h*ll*"
# scan returns the matched values
"hello".scan(/[aeiou]/)
# => ["e", "o"]
The three comparisons above highlight when each method is the right choice. split is cleanest for delimiter-based splitting, match returns rich MatchData for a single match, and gsub transforms the string instead of collecting matches. scan sits in the middle: it gathers every match without transforming the original text, which makes it the natural pick when you need all occurrences as a flat collection for further processing or counting.
See Also
- /reference/string-methods/match/ — find the first match, returns MatchData
- /reference/string-methods/gsub-bang/ — find and replace all occurrences
- /reference/string-methods/sub/ — replace only the first occurrence