Find & Replace — Ruby source
Find and replace text with literal or regular-expression matching, global replace, case sensitivity, whole-word, and capture-group substitution. Live match counter.
This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# frozen_string_literal: true
# Find & replace with literal or regex matching, $-substitution
# ($1 backrefs, $&, $$), case sensitivity, whole-word, and global modes.
#
# Language: Ruby (3.2+, standard library only)
# Source: CosmoDev polyglot showcase port of the find-replace tool,
# ported from src/lib/findReplace.ts (the canonical TypeScript
# implementation).
# License: display source — part of CosmoDev's polyglot tool pages.
#
# Mirrors the live lib: a literal find string is Regexp.escape'd and matched
# verbatim; an isRegex find is compiled as-is. \b wraps the pattern when
# whole_word is set, and Regexp::IGNORECASE covers the JS i flag. The JS m
# flag makes ^ and $ anchor at line breaks — Ruby's ^ and $ ALWAYS do, so no
# flag maps it (a documented approximation: this port is line-anchored where
# the lib would restrict ^/$ to the string bounds when multiline is off).
# RegexpError is caught and returned as the error string (the lib never
# raises), and an empty find is a no-op.
#
# Replacement $-substitution is implemented in expand_match (not gsub's \1)
# so it matches JavaScript's String.replace exactly for the realistic cases:
# $$ -> $, $& -> whole match, $1..$99 -> capture group (literal "$<digits>"
# when out of range). JS's $` and $' are unsupported.
module FindReplace
Result = Struct.new(:result, :matches, :error, keyword_init: true)
module_function
# Escape metacharacters for literals, wrap \b..\b for whole-word.
def build_pattern(find, is_regex:, whole_word:)
pattern = is_regex ? find : Regexp.escape(find)
whole_word ? "\\b#{pattern}\\b" : pattern
end
# Compile the pattern with the case flag. Invalid syntax returns the
# engine's message (the lib's error path), never a raise.
def build_regex(find, is_regex:, case_sensitive:, whole_word:)
flags = 0
flags |= Regexp::IGNORECASE unless case_sensitive
Regexp.new(build_pattern(find, is_regex: is_regex, whole_word: whole_word), flags)
rescue RegexpError => e
e.message
end
# Apply JS String.replace $-substitution for one match.
# "$$" -> "$"; "$&" -> whole match; "$1".."$99" -> capture group N
# (literal "$<digits>" when N is out of range, matching JS).
# groups[0] is the whole match; m[n] is nil for a group that did not
# participate, which becomes "" as in JS.
def expand_match(template, match)
num_groups = match.size - 1
groups = (0..num_groups).map { |i| match[i] || '' }
out = +''
i = 0
n = template.length
while i < n
c = template[i]
if c != '$'
out << c
i += 1
next
end
nxt = i + 1 < n ? template[i + 1] : nil
case nxt
when '$'
out << '$'
i += 2
when '&'
out << groups[0]
i += 2
when '0'..'9'
d1 = nxt.ord - 48
# Greedily try a second digit ($nn), matching JS.
if i + 2 < n && ('0'..'9').cover?(template[i + 2])
d2 = d1 * 10 + (template[i + 2].ord - 48)
if d2.between?(1, num_groups)
out << groups[d2]
i += 3
next
end
end
if d1.between?(1, num_groups)
out << groups[d1]
i += 2
else
out << '$' << nxt
i += 2
end
else
out << '$'
i += 1
end
end
out
end
# Replace occurrences of +find+ with +replacement+. Never raises.
def find_replace(input, find, replacement, is_regex: false, case_sensitive: true,
whole_word: false, global: true, multiline: false)
return Result.new(result: input, matches: 0, error: nil) if find.empty?
built = build_regex(find, is_regex: is_regex, case_sensitive: case_sensitive,
whole_word: whole_word)
return Result.new(result: input, matches: 0, error: built) if built.is_a?(String)
pattern = built
out = +''
matches = 0
num_groups = 0
pos = 0
while (m = pattern.match(input, pos))
num_groups = m.size - 1
out << input[pos...m.begin(0)]
out << expand_match(replacement, m)
matches += 1
pos = m.end(0)
break unless global
if pos == m.begin(0) # empty match: copy one char and step, like JS
out << input[pos] if pos < input.length
pos += 1
end
end
out << input[pos..] if pos <= input.length
matches = 1 + num_groups if matches != 0 && !global # JS String.match quirk
Result.new(result: out, matches: matches, error: nil)
end
end
if $PROGRAM_NAME == __FILE__
r = FindReplace.find_replace('Hello World world', 'world', 'Universe',
case_sensitive: false)
if r.error
puts "error: #{r.error}"
else
puts "#{r.result} (#{r.matches} matches)"
end
end
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →