gsub and String Substitution

Published by Programming Line in Ruby. Last updated .

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 \1 looks like a Ruby string escape, it’s safest written inside single quotes so Ruby doesn’t try to interpret it before gsub ever 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 = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", '"' => "&quot;" }
  str.gsub(/[&<">]/, replacements)
end

puts escape_html('<p class="intro">Tom & Jerry</p>')

Output:

&lt;p class=&quot;intro&quot;&gt;Tom &amp; Jerry&lt;/p&gt;

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

  1. Ruby starts scanning the string from the left, looking for the next position where the pattern matches.
  2. 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.
  3. The matched text is swapped for that replacement in the result being assembled; any surrounding, non-matching text is copied over unchanged.
  4. Scanning resumes immediately after the matched text, never inside it, so matches never overlap.
  5. This repeats until Ruby reaches the end of the string. The fully assembled string is then returned as a brand-new object. gsub never touches the original string; gsub! writes the result back into the original object in place, and returns nil instead 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 of gsub/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.escape before 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 before gsub sees 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! or sub!; they return nil when nothing changed. Either check the return value or fall back with ||.
  • For a simple literal search-and-replace, pass a plain String pattern instead of a Regexp — it’s clearer and avoids accidentally triggering regex metacharacter behavior.

Practice Exercises

  1. Write a method censor(text) that replaces every vowel (a, e, i, o, u, in either case) in text with an asterisk, using gsub with a regex character class. Try it on "Hello World" and trace the result by hand before running it.
  2. Write a method snake_to_camel(text) that converts a snake_case string like "first_name_field" into camelCase ("firstNameField"), using gsub with 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.
  3. 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 single gsub call, building the regex pattern from the hash’s keys and passing the hash itself as the replacement argument.

Summary

  • gsub replaces every match of a pattern in a string and returns a new string; it never modifies the original.
  • The pattern can be a literal String or a Regexp; the replacement can be a String with \1/\2/\& backreferences, a block, or a Hash.
  • gsub! mutates the string in place but returns nil if no substitution was made — never chain off its result blindly.
  • sub and sub! behave like gsub and gsub! but only touch the first match.
  • Always escape user-supplied or variable text with Regexp.escape before dropping it into a regex, unless its characters are meant to act as regex metacharacters.