I lost an hour to a bug that produced zero errors, zero crashes, and zero useful feedback. Every blog post on my site rendered with a title and... nothing else. Empty content. Here's what happened.
The Setup
I built a static site generator in Ruby. ~100 lines. Markdown files with YAML frontmatter go in, HTML pages come out. Standard stuff.
The build script ran fine. 3 posts built. No errors. But every post was empty.
The Bug
def parse_frontmatter(content)
if content =~ /\A---\n(.*?)---\n(.*)\z/m
meta = {}
$1.each_line do |line|
key, val = line.split(':', 2).map(&:strip)
val = val.gsub(/^["']|["']$/, '') # strip quotes
meta[key] = val
end
[meta, $2]
end
end
See it? Line 5 calls val.gsub with a regex. In Ruby, every regex operation resets the global match variables $1, $2, etc.
By the time we return [meta, $2], $2 isn't the blog post body anymore. It's whatever the last gsub captured — which is nil, because that regex has no capture groups.
The body is gone. The build succeeds. The site is empty.
The Fix
One line:
def parse_frontmatter(content)
if content =~ /\A---\n(.*?)---\n(.*)\z/m
front = $1 # capture immediately!
body = $2 # before anything else touches match vars
meta = {}
front.each_line do |line|
key, val = line.split(':', 2).map(&:strip)
val = val.gsub(/^["']|["']$/, '')
meta[key] = val
end
[meta, body]
end
end
Capture $1 and $2 into local variables immediately after the match. Before any other code runs.
Why This Is Dangerous
$1, $2, etc. are thread-local global variables that get silently overwritten by:
String#gsubString#subString#matchString#=~String#scan- Any method that internally uses regex
They survive across lines but not across regex operations. And nothing warns you when they get clobbered.
Safer Alternatives
Named captures:
if content =~ /\A---\n(?<front>.*?)---\n(?<body>.*)\z/m
front = $~[:front]
body = $~[:body]
end
Regexp.last_match:
if content =~ /\A---\n(.*?)---\n(.*)\z/m
m = Regexp.last_match
front = m[1]
body = m[2]
end
String#match with block:
content.match(/\A---\n(.*?)---\n(.*)\z/m) do |m|
front = m[1]
body = m[2]
end
The Lesson
The most dangerous bugs are the ones that don't crash. This one:
- Produced valid output (HTML files with titles)
- Reported success (
3 posts built) - Had no error messages
- Only manifested as missing content
If you use Ruby's $1/$2 globals, capture them on the very next line. Or better yet, use named captures or match with a block.
This was a real bug I hit while building a blog with a 100-line Ruby static site generator. Three deploys, one hour, zero error messages.
Top comments (0)