skip to content

Regexp Matching

Ruby matches text with =~, match and match?, exposes captures through MatchData and $~, and rewrites with gsub. Interviewers probe \A versus ^ anchors and Regexp.timeout against ReDoS.

on this pageshow

explore

questions

5

In Ruby, how do the results of String#=~, String#match and String#match? differ, and which of them set $~ and $1?

level: juniorimportance: must knowfreq 62%

answer

  1. index, MatchData, boolean
  2. nil when nothing matches
  3. match? leaves the globals alone
  4. $~ is frame-local and thread-local
  5. String =~ String raises TypeError

basics

~20 s

=~ returns the index where the match starts or nil, match returns a MatchData or nil, and match? returns true or false. =~ and match set $~, $1 and the other match variables; match? sets none of them.

solid answer

~40 s

All three take a Regexp. `"Car AB-123" =~ /[A-Z]{2}-\d{3}/` returns `4`, the start index, and index `0` is still truthy in Ruby. `match` returns a `MatchData` whose `[]` gives the captures, or `nil`. `match?` returns `true` or `false` without building a `MatchData`, which makes it the cheapest yes/no test. `=~` and `match` store the result in `$~`, and `$1`, `$&`, `` $` `` and `$'` are read from it; `match?` leaves them untouched, so reading `$1` after `match?` returns whatever an earlier match left. `$~` is local to the current method frame and thread, so a helper method's match does not change the caller's `$1`. Two traps: `String#=~` with a String argument raises `TypeError`, and `String#match` compiles a String argument into a Regexp, so `"."` matches any character.

code

ruby · 10 lines
ruby
PLATE = /([A-Z]{2})-(\d{3})/

"Car AB-123" =~ PLATE          # => 4
$1                              # => "AB"
"Car AB-123".match(PLATE)      # => #<MatchData "AB-123" 1:"AB" 2:"123">
"Car AB-123".match?(PLATE)     # => true
"no plate".match(PLATE)        # => nil
$1                              # => nil, the failed match cleared it
"Car CD-456".match?(PLATE)     # => true
$1                              # => nil, match? did not set it

go deeper

for a junior

Recall the three return types, index, MatchData and boolean, all nil or false on a miss, and that match? does not set $1.

for a middle

Explain where $1 comes from, why $~ is frame-local and thread-local, and why reading MatchData is clearer than reading the $ variables.

for a senior

Prefer match? in hot validation paths, avoid logic that depends on $~ surviving between calls, and escape user-supplied text before it becomes a pattern.

for a principal

Set team conventions: MatchData locals over $ globals, match? for predicates, and a lint or review rule against building patterns from unescaped input.

## Three ways to ask whether text matches Ruby gives `String` (and `Regexp` and `Symbol`) three matching methods that differ in what they return and in what they record. | Call | Returns on a match | Returns on no match | Sets `$~`, `$1`… | |---|---|---|---| | `str =~ re` | Integer start index | `nil` | yes | | `str.match(re)` | `MatchData` | `nil` | yes | | `str.match?(re)` | `true` | `false` | no | - **`=~`** is an operator that returns the character index where the match begins. Because only `nil` and `false` are falsy, `if str =~ re` works even when the match starts at index `0`. - **`match`** returns a `MatchData` object, the full record of the match: the matched text, every capture, and their positions. It also takes an optional start offset and, given a block, yields the `MatchData` only when there is a match. - **`match?`** returns a plain boolean. It neither allocates a `MatchData` nor updates any match variable, so it is the right call for validation in a hot path. ## What gets recorded: `$~` and friends `=~` and `match` store their `MatchData` in the special variable **`$~`**. The other match variables are shorthands read from it: - `$&` is the whole matched text, `` $` `` the text before it and `$'` the text after it; - `$1`, `$2`, … are the numbered capture groups, and `$+` is the last group that matched; - `Regexp.last_match` returns the same object as `$~`, and `Regexp.last_match(1)` the first group. When a match fails, `$~` becomes `nil`, and so do all the shorthands. ## `$~` is not really global Despite the `$`, `$~` is **frame-local and thread-local**: 1. A method that runs a regexp sets `$~` only for its own frame; when it returns, the caller's `$~` and `$1` are exactly as they were. 2. A block shares the frame of the method it is written in, so a match inside an `each` block does update that method's `$1`. 3. Each thread has its own `$~`, so two threads matching concurrently cannot overwrite each other's captures. ```ruby def region_of(text) text =~ /([A-Z]{2})-\d{3}/ $1 end "XY" =~ /(XY)/ region_of("AB-123") # => "AB" $1 # => "XY", the caller's frame is untouched ``` ## Readable names and safer habits The punctuation variables have longer aliases in the `English` library, a default gem: after `require "English"`, `$LAST_MATCH_INFO` is `$~`, `$MATCH` is `$&`, `$PREMATCH` and `$POSTMATCH` are `` $` `` and `$'`, and `$LAST_PAREN_MATCH` is `$+`. They are easier to read in code review, but they are the same frame-local values. Most style guides go further and avoid the match variables in application code altogether: - assign the `MatchData` to a local, `md = PLATE.match(input)`, and read `md[1]` or a named capture; - keep the check and the read together, so no other match can run between them; - use `match?` when no capture is read at all, which also saves the `MatchData` allocation on every call. Code written this way survives refactoring: moving the match into a helper method no longer silently changes what `$1` means in the caller. ## Choosing one 1. **Only need yes or no?** Use `match?`; it is the cheapest and has no side effects. 2. **Need the captures?** Use `match` and read the returned `MatchData` rather than the `$` variables, so the data flows through a named local. 3. **Need the position?** `=~` gives the index directly; `MatchData#begin(0)` gives the same from `match`. ## Traps - `"AB-123" =~ "AB"` raises `TypeError` (`type mismatch: String given`): `String#=~` wants a Regexp. - `"AB-123".match(".")` returns a match on `"A"`: `match` turns a String argument into a Regexp without escaping it, unlike `sub`, `gsub` and `scan`, which treat a String pattern as literal text. Pass a Regexp, or escape user text with `Regexp.escape`. - Checking with `match?` and then reading `$1` reads a stale value from an earlier match in the same method, or `nil`.

  • In Ruby, does a regexp match inside a block passed to each change the enclosing method's $1?
    Yes. A block runs in the frame of the method where it is written, so `$~` and `$1` set inside it are the method's own. A match inside a separate method, by contrast, does not reach the caller, and a match in another thread never touches this thread's `$~`.
  • In Ruby, why does "AB-123".match(".") return a MatchData for "A"?
    `String#match` converts a String argument with `Regexp.new`, so `"."` becomes the any-character pattern. `sub`, `gsub` and `scan` quote a String pattern instead. To match literal text from a user, pass `Regexp.new(Regexp.escape(text))` or, when only a yes/no answer is needed, use `include?`.

saying these in an interview costs you the question

  • match? sets $~ just like match and only returns a boolean
  • =~ returns true or false
  • A regexp match inside a helper method overwrites the caller's $1
  • String#match treats a String argument as literal text
  • $~ is one process-wide variable that threads overwrite
open as a page

In Ruby, why does /^[A-Z]{2}-\d{3}$/ accept "AB-123\n<script>" as a licence plate, and how do \A and \z fix it?

level: middleimportance: must knowfreq 58%

basics

~20 s

Ruby's ^ and $ match at the start and end of every line, so a value whose first line is a valid plate passes. \A and \z match only the whole string's start and end, so /\A[A-Z]{2}-\d{3}\z/ rejects extra lines.

open as a page

In Ruby, how do String#sub and String#gsub behave when given a replacement string, a Hash, or a block?

level: juniorimportance: should knowfreq 55%

basics

~20 s

sub replaces the first match and gsub every match, each returning a new string. A replacement string may use back-references such as \1 or \k<name>, a Hash maps matched text to replacements, and a block computes each replacement.

open as a page

In Ruby, how do you extract the region code and serial number from a licence plate using named captures and MatchData?

level: middleimportance: should knowfreq 45%

basics

~10 s

Name each group with (?<region>…) and (?<number>…), call match, and read md[:region] and md[:number] from the MatchData, after checking it is not nil. named_captures returns all of them as a Hash.

open as a page

In Ruby 4.0, how do Regexp.timeout, Regexp.new's timeout: keyword and Regexp.linear_time? help protect a service that matches untrusted input?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Regexp.timeout= sets a process-wide limit in seconds, Regexp.new(src, timeout:) sets one per pattern, and an overrun raises Regexp::TimeoutError. Regexp.linear_time? reports whether this interpreter can match a pattern in linear time, for checking your own patterns.

open as a page