Update your entire project to Ruby 1.9 hash syntax

In version 1.9 Ruby introduced new syntax for hash literals whose keys are symbols. Hashes use the "hash rocket" operator to separate the key and the value:

my_hash = { :key => 'value' }

In Ruby 1.9 that syntax is still valid, but when the key is a symbol we can also write it like this:

my_hash = { key: 'value' }

If you no longer need to support Ruby 1.8 it's worth updating your code. The new syntax is easier to read and quicker to type. Updating an entire project by hand would be painful, but with the Unix command line and a quick regular expression we can make the computer do the hard work.

So our task is to write a small program that will find all occurrences of :key => and convert them to key:, in all the Ruby files in our project.

Easy, right?

Finding all the Ruby files

We're on Unix (well I am, at least), so we don't actually need to write any code. We can use the find command instead, from the top level of our project:

$ find . -name \*.rb

If we can cook up a command that can replace all the old-style hash syntax in a single file, we can run it against all the Ruby files with the -exec option to find.

$ find . -name \*.rb -exec our_command {} \;

The {} characters will be substituted for the name of the filename, while \; is used to tell find that we've finished specifying the command.

Writing a command to update a single file

There are lots of different ways to approach this, but regular expressions seem like a good fit. We could use sed, a classic Unix utility with decent regex support. Or we could use a one line Ruby command, with it's very powerful regular expression engine and syntax.

I tend to use Perl for tasks like this, which falls somewhere inbetween sed's concise nature and Ruby's power.

Let's build a Perl command to do the job. If we run perl with the -p and -e switches it'll execute a snippet of Perl code for us, reading data from STDIN and printing out the results. Due to the way -p works, we only need to specify a regex substitution command, and Perl will run it on each line of input that we give it.

To get us started, let's replace any text that matches a colon, followed by one or more alphanumeric characters and a hash rocket, with the alphanumeric string followed by a colon:

$ echo "{ :foo => 'bar' }" | perl -p -e 's/:(\w+)\s*=>/\1:/'
{ foo: 'bar' }

So far so good, but it's a little brittle. It'll break if it encounters a hash key that's a namespaced class, like MyModule::Class.

$ echo "MyModule::MyClass =>" | perl -p -e 's/:(\w+)\s*=>/\1:/'
MyModule:MyClass:

We can fix that by requiring that the character immediately before the colon can't be another colon, making sure that we insert this preceding character back into the output (that's what the round brackets around [^:] are for):

$ echo "MyModule::MyClass =>" | perl -p -e 's/([^:]):(\w+)\s*=>/\1\2:/'
MyModule::MyClass =>

Let's just check it still works on hashes...

$ echo "{ :foo => 'bar' }" | perl -p -e 's/([^:]):(\w+)\s*=>/\1\2:/'
{ foo: 'bar' }

Good stuff.

The final solution

Now all we need to do is to run it on every file in our project, by running perl inside the find command:

$ find . -name \*.rb -exec perl -p -i -e 's/([^:]):(\w+)\s*=>/\1\2:/g' {} \;

The eagle-eyed reader will have noticed that I've slipped the -i switch into the Perl command. It tells Perl to modify files "in place".

Before you run it, just make sure that you've checked your code in! Then you can review the diff, run your tests, and throw all the changes away if you find that the regex needs further improvements on your code base (let's face it, I'm not likely to have thought of everything).

I love feedback and questions — please feel free to get in touch on Mastodon or Twitter, or leave a comment.