Ruby
Lets Write Some Bad Ruby
One of my favorite things to do with Ruby is to write a quick throw away script to automate some mechanical changes to my code that can't easily be done with a text editor. Unlike normal code, the code can be downright bad and it's perfectly fine, because once the script runs, we can just delete it.
Sometimes I Don't Want To Type All That
All we care about is the computer doing something for us in a few seconds that would take us much, much longer manually. Here's an example in Rails:
Rails.application.routes.draw do
namespace :foo do
resources :bars
resources :users
# plus 20 more resources
end
end
Let's say that for whatever reason, namespacing our resources under 'foo' seemed like the correct decision when we were building our app, but now we've decided not to. We're going to move them out.
Rails.application.routes.draw do
resources :bars
resources :users
# plus 20 more resources
end
We know this is going to break our code in a few ways:
The route helpers. Our
bars/index
route wasfoo_bars_path
, but now it's simplybars_path
. The same concept applies to every other route, and these are bound to be all over our code.Controller namespacing. Our
BarsController
looks like,class Foo::BarsController < ApplicationController
, so we need to drop theFoo::
namespace off of our controllers.We need to move everything out of
app/**/foo/**
, toapp/**/**
When dealing with a couple of resources, this isn't really a big deal. But when you have 10, 20, or more resources, doing all that by hand is slow. Sometimes we can accomplish these changes with a text editor, but since we'll have a bunch of path helpers that will all be different (and we'll also be moving files), there will still be a good chunk of manual editing. Luckily, we can write a throwaway script to do it for us!
The Throwaway Script
# cd to root of app
$ touch tmp/move_stuff.rb && cd tmp
We know that our changes are primarily breaking controllers and views. Let's start with views. We want to recursively loop through each view namespaced under foo
. For this, we'll use Dir.glob
. We'll glob our directories, check to make sure the current file in the iteration isn't a directory, and then find & change our path helpers.
#tmp/move_stuff.rb
Dir.glob '../app/views/foo/**/*' do |file|
if !Dir.exist? file
text = File.read file
text.gsub! /(\w*)foo_(\w+)path/, '\1\2path'
#puts text.gsub /(\w*)foo_(\w+)path/, '\1\2path'
File.open(file, 'w') { |f| f.write text }
end
end
Here we read the file, find anything that matches the format of a path helper that had the foo namespace, gsub it with the first and second captures, and append 'path' to it. Then, we write the file back. Notice, I included the code I used to test the regex when I ran the script, which logged it to the console instead of actually changing anything. I don't want to make any changes until I'm sure I have it right. This applies to any step here.
We also we want to move the file. The current path is the current item in the iteration, file
. We can simply gsub out 'foo/' to get the path we want.
file
# '../app/views/foo/bars/index.html.haml'
new_path = file.gsub "foo/", ""
# '../app/views/bars/index.html.haml'
We can get the directory in the new path by matching anything that has word characters followed by a trailing forward slash, like this:
dir_to_move_to = new_path.gsub /.+\//, ""
# '../app/views/bars/'
We can't move to a directory that isn't there, so make it if it doesn't exist:
Dir.mkdir dir_to_move_to unless Dir.exists? dir_to_move_to
And finally, backtick a console mv
command to move the file to the new_path:
`mv #{file} #{new_path}`
# `mv ../app/views/foo/bars/index.html.haml ../app/views/bars/index.html.haml`
The final product:
Dir.glob '../app/views/foo/**/*' do |file|
if !Dir.exist? file
text = File.read file
text.gsub! /(\w*)foo_(\w+)path/, '\1\2path'
File.open(file, 'w') { |f| f.write text }
new_path = file.gsub "foo/", ""
dir_to_move_to = new_path.gsub /.+\//, ""
Dir.mkdir dir_to_move_to unless Dir.exists? dir_to_move_to
`mv #{file} #{new_path}`
end
end
Then go to the /tmp
directory and run the file (it needs to be in /tmp
in order to use the relative pathnames we wrote, which is probably bad, but I don't care. I'm deleting this in 12 seconds.)
$ ruby move_stuff.rb
And that takes care of the views.
In Conclusion: Yuck
WOW that is some ugly code! The future reader of this code would have to stare at it for a minute or two before getting a handle on what's going on. But here, ugly code is the right tool for the job, because it can be written quickly, and once this script runs, we'll never need to run this again, so we can just delete it. I think this is much easier than doing it by hand (assuming you have a lot of files), plus, the more you write these types of scripts, the quicker you can write them in the future.
For the controllers, you could literally copy and paste (remember, we're deleting this!) the same chunk of code, replace views
with controllers
, and add a gsub for removing the "Foo::" namespacing.
When you're done, delete this file and never speak of it again!