Write Fewer Regular Expressions

Mike Burns

Oh man Cucumber is awesome but why do I have to write regular expressions? It’s always like:

Then /^show me the emails$/ do
  puts ActionMailer::Base.deliveries.map do |email|
    email.subject
  end.join("\n")
end

Then /^I should see the "([^"]+)" link once$/ do |link_title|
  assert_select '.deal_title', :text => link_title, :count => 1
end

Then /^I should see "([^"]+)" for "([^"]+)" (.*) field
  in the response XML$/ do |value, xpath, field|
  @parsed_response.xpath("#{xpath}[@#{field}='#{value}']").should_not be_empty
end

Ugh that’s so ugly and it feels like I’m writing awk or Perl, and while I love me a good awk script it really does not belong near my beautiful Ruby. (They’re so ugly that they broke the syntax highlighter!)

Well you probably know how to replace the first one with a string:

Then 'show me the emails' do
  puts ActionMailer::Base.deliveries.map do |email|
    email.subject
  end.join("\n")
end

But what of our parameterized steps?

I had a vision, a vision of a string with printf-like escapes inside a string. I was pumped and ready to make a patch so I cloned the source off github and started looking through the classes (this was about 11PM). That’s when I discovered that this was done for me by the wonderful scientists at the Cucumber Research Institute! We can re-write those steps like this:

Then 'I should see the "$link_title" link once' do |link_title|
  assert_select '.deal_title', :text => link_title, :count => 1
end

Then 'I should see "$value" for "$xpath" $field field
  in the response XML' do |value, xpath, field|
  @parsed_response.xpath("#{xpath}[@#{field}='#{value}']").should_not be_empty
end

Here’s how it works: the Cucumber step parser for Ruby turns every string into a regular expression, to reduce it to a previously-solved problem. On the way it translates $foo into (.*). That’s it!

if String === regexp
  p = Regexp.escape(regexp)
  p = p.gsub(/\\\$\w+/, '(.*)') # Replace $var with (.*)
  regexp = Regexp.new("^#{p}$")
end

This, obviously, is not going to rid the world of regular expressions overnight. Take these two for examples:

Given /^(?:|I )am on (.+)$/ do |page_name|
  visit path_to(page_name)
end

Given /^the user with email "([^\"]*)" is (not )?an admin$/ do |email, status|
  user = User.find_by_email!(email)
  user.admin = status.blank?
  user.save!
end

But these steps are few and far between.

Only you can reduce your regexps. Replace them with strings, today!