gsub and String Substitution
Ruby’s gsub method (short for global substitution) scans a string for every match of a pattern and replaces each one, returning a brand-new string with the substitutions applied. It’s the tool you reach for whenever you need to clean up user input, redact sensitive data, reformat text, or swap placeholders inside a template. Because gsub accepts a plain string, a full regular expression, a block, or even a hash of replacements, it scales smoothly from a one-line search-and-replace all the way up to fairly sophisticated text transformations.
Overview: How gsub Works
Every Ruby string is an object, and gsub is just a regular method defined on the String class. Like everything else in Ruby — including integers, nil, and even if expressions — calling a method produces a value rather than triggering some special compiler-level operation. That matters here because str.gsub(...) does not change str at all: it builds and returns a completely new String object, leaving the original exactly as it was. If you want to keep the result, you have to capture it, usually by reassigning it back to the same variable: str = str.gsub(...).
Every call needs a pattern to search for and something that says what each match becomes.
The pattern is either:
- A plain
String— gsub looks for that exact literal substring, and no characters are treated as special. - A
Regexp, written between slashes like/\d+/— this lets you match whole families of text: digits, whitespace, optional characters, alternatives, and captured groups.
What replaces each match can be:
- A
String, inserted as-is — except Ruby recognizes backreferences inside it:\1,\2, and so on refer to a regex’s captured groups, and\&refers to the entire match. Because\1looks like a Ruby string escape, it’s safest written inside single quotes so Ruby doesn’t try to interpret it beforegsubever sees it. - A block, which receives each matched substring in turn; whatever the block returns becomes that match’s replacement. This is the most flexible form, since you can run arbitrary logic per match.
- A
Hash, where each matched substring is looked up as a key and the corresponding value is substituted in. If a match isn’t found as a key, it’s replaced with an empty string — so this form works best when your regex only ever matches things the hash actually covers.
gsub has a mutating sibling, gsub!, which modifies the receiver in place and returns the string itself — but only if at least one substitution actually happened. If nothing matched, gsub! returns nil instead, which is a well-known Ruby footgun covered in Common Mistakes below. Ruby also has sub and sub!, which behave exactly like gsub and gsub! except they only replace the first match instead of every match.
| Method | Replaces | Mutates receiver? | Returns |
|---|---|---|---|
sub |
First match only | No | A new string |
sub! |
First match only | Yes | self, or nil if nothing changed |
gsub |
Every match | No | A new string |
gsub! |
Every match | Yes | self, or nil if nothing changed |
Syntax
The general shapes of a gsub call are shown below. pattern is a String or Regexp; replacement is typically a String; the block and hash forms are used when a fixed replacement string isn’t expressive enough.
str.gsub(pattern, replacement)
str.gsub(pattern) { |match| replacement_string }
str.gsub(pattern, hash_of_replacements)
str.gsub!(pattern, replacement)
| Form | What it does |
|---|---|
str.gsub(pattern, replacement) |
Replaces every match of pattern with replacement; the replacement string may use \1, \2, and \& backreferences. |
str.gsub(pattern) { |match| ... } |
Replaces every match with whatever the block returns for that match. |
str.gsub(pattern, hash) |
Replaces every match with hash[match], or an empty string if the match isn’t a key. |
str.gsub!(pattern, replacement) |
Same as gsub, but mutates str in place and returns nil if nothing changed. |
Examples
Example 1: Literal Substitution
The simplest form of gsub takes two strings: what to find, and what to put in its place. Every occurrence of the search string is replaced — there’s no regular expression involved at all.
text = "The cat sat on the mat"
result = text.gsub("at", "og")
puts result
Output:
The cog sog on the mog
Ruby scans the string left to right and swaps every literal occurrence of at for og — inside cat, sat, and mat — leaving The and the untouched, since neither contains the substring at.
Example 2: Regex and a Block
Real text usually needs pattern matching rather than a literal search. Here, a regular expression matches any http or https URL, and a block decides the replacement for every match it finds.
sentence = "Visit https://example.com or http://test.org for details"
result = sentence.gsub(/https?:\/\/(\S+)/) { |match| "[link removed]" }
puts result
Output:
Visit [link removed] or [link removed] for details
https? matches either http or https, and \S+ greedily grabs every non-whitespace character that follows — the rest of the URL. Both URLs are found and replaced with a fixed marker string returned by the block, regardless of which one actually matched.
Example 3: Hash-Based Substitution
When several different matched characters each need their own replacement, a Hash is often cleaner than chaining multiple gsub calls together. This example HTML-escapes four special characters in a single pass — essentially how many HTML-escaping helpers work under the hood.
def escape_html(str)
replacements = { "&" => "&", "<" => "<", ">" => ">", '"' => """ }
str.gsub(/[&<">]/, replacements)
end
puts escape_html('<p class="intro">Tom & Jerry</p>')
Output:
<p class="intro">Tom & Jerry</p>
The regex character class [&<">] matches any single occurrence of &, <, >, or ". For every character it matches, gsub looks that one character up as a key in the hash and inserts the corresponding entity string in its place.
Example 4: Backreferences to Rearrange a Match
Capture groups combined with backreferences let you rearrange the pieces of a match rather than replacing it outright.
text = "first-last"
swapped = text.gsub(/(\w+)-(\w+)/, '\2-\1')
puts swapped
Output:
last-first
(\w+) greedily matches the run of word characters before the hyphen, capturing it as group 1; the second (\w+) captures the run of word characters after it as group 2. The replacement string '\2-\1' writes group 2 first, then group 1 — swapping the two halves. Notice it’s written with single quotes: inside a double-quoted string, Ruby would try to interpret \1 itself before gsub ever saw it.
How gsub Works, Step by Step
- Ruby starts scanning the string from the left, looking for the next position where the pattern matches.
- Once a match is found, Ruby works out its replacement: evaluating backreferences in a replacement string, calling the block with the matched text, or looking the matched text up in a hash.
- The matched text is swapped for that replacement in the result being assembled; any surrounding, non-matching text is copied over unchanged.
- Scanning resumes immediately after the matched text, never inside it, so matches never overlap.
- This repeats until Ruby reaches the end of the string. The fully assembled string is then returned as a brand-new object.
gsubnever touches the original string;gsub!writes the result back into the original object in place, and returnsnilinstead of the string if no replacement happened at all.
Common Mistakes
Mistake 1: Assuming gsub Mutates the String
It’s tempting to call gsub expecting the original variable to change, the way an in-place method might behave in another language:
text = "hello world"
text.gsub("world", "Ruby")
puts text
Output:
hello world
gsub always returns a new string rather than modifying its receiver, so text is completely unaffected — the return value was simply discarded. Either capture the result by reassigning it, or use gsub! if you genuinely want to mutate in place:
text = "hello world"
text = text.gsub("world", "Ruby")
puts text
Output:
hello Ruby
Mistake 2: Chaining Off gsub! Without Checking for nil
gsub! looks convenient because it mutates in place, but it has a sharp edge: if no substitution was actually made, it returns nil instead of the string. Calling another method directly on that result blows up:
text = "hello world"
result = text.gsub!("xyz", "abc")
puts result.upcase
Since text never contains xyz, no substitution happens, so gsub! returns nil — and calling .upcase on nil raises NoMethodError before anything is ever printed. Guard against this by falling back to the original string with ||, or by checking the return value first:
text = "hello world"
result = text.gsub!("xyz", "abc") || text
puts result.upcase
Output:
HELLO WORLD
Mistake 3: Interpolating Unescaped Text Into a Regex
It’s easy to build a regex out of a variable with interpolation, but if that variable contains regex metacharacters — like ., $, *, or ( — they’re interpreted as regex syntax instead of literal characters:
def redact(text, word)
text.gsub(/#{word}/, "***")
end
puts redact("Cost is $5.00 (discounted)", "$5.00")
Output:
Cost is $5.00 (discounted)
The word to redact, $5.00, gets interpolated straight into the regex, producing the pattern /$5.00/. There, $ means end-of-line and . means any character, so the pattern actually asks for the literal text 5.00 to appear immediately after the end of the line — something that can never happen. The regex silently matches nothing, so the price is never redacted and no error is raised. Escaping the variable with Regexp.escape before interpolating it fixes this by treating every character in it literally:
def redact(text, word)
text.gsub(/#{Regexp.escape(word)}/, "***")
end
puts redact("Cost is $5.00 (discounted)", "$5.00")
Output:
Cost is *** (discounted)
Best Practices
- Use
sub/sub!instead ofgsub/gsub!when you only want to replace the first occurrence — it communicates intent and avoids surprises on repeated matches. - Escape user-supplied or variable text with
Regexp.escapebefore interpolating it into a regex, unless you deliberately want its characters to act as regex metacharacters. - Write backreference replacement strings (
\1,\2) with single quotes so Ruby doesn’t try to interpret the backslash sequence beforegsubsees it. - Reach for the block form when the replacement depends on the match itself — capitalizing it, looking it up, transforming it — rather than trying to force everything into one static replacement string.
- Never chain a method directly off the result of
gsub!orsub!; they returnnilwhen nothing changed. Either check the return value or fall back with||. - For a simple literal search-and-replace, pass a plain
Stringpattern instead of aRegexp— it’s clearer and avoids accidentally triggering regex metacharacter behavior.
Practice Exercises
- Write a method
censor(text)that replaces every vowel (a,e,i,o,u, in either case) intextwith an asterisk, usinggsubwith a regex character class. Try it on"Hello World"and trace the result by hand before running it. - Write a method
snake_to_camel(text)that converts a snake_case string like"first_name_field"into camelCase ("firstNameField"), usinggsubwith a regex and a block that uppercases the letter following each underscore. Hint: match the underscore together with the letter after it in one capture. - Given a hash such as
{ "cat" => "dog", "sun" => "moon" }, write a method that replaces every occurrence of any key in a sentence with its corresponding value in a singlegsubcall, building the regex pattern from the hash’s keys and passing the hash itself as the replacement argument.
Summary
gsubreplaces every match of a pattern in a string and returns a new string; it never modifies the original.- The pattern can be a literal
Stringor aRegexp; the replacement can be aStringwith\1/\2/\&backreferences, a block, or aHash. gsub!mutates the string in place but returnsnilif no substitution was made — never chain off its result blindly.subandsub!behave likegsubandgsub!but only touch the first match.- Always escape user-supplied or variable text with
Regexp.escapebefore dropping it into a regex, unless its characters are meant to act as regex metacharacters.
