Jan Brauer

Applying refactorings with sed

Once in a while you may need to do a big refactoring that neither your IDE nor your favorite editor can handle properly. Now you’re left with three options. Leave it to your colleagues, manually edit each file or learn sed.

In this very blog post I’ll show you how to apply the “Extract Method” refactoring across multiple files. Let’s assume we have a bunch of classes containing some duplications we want to remove.

class Foo
    def initialize
	self.bar
	self.baz
	self.foo
    end
end

class Bar
    def initialize
	self.bar
	self.baz
	self.foo
    end
end

After grepping through our code base, we decide on extracting a method called bar_baz_foo. The question is, how to do that? The answer: multi line search and replace. This is were sed comes into play. Sed is a stream editor that performs editing operations on an input stream. Most people will have seen the common substitute command either disguised as a Perl regex, a vi command or in plain sed

# Replace occurrence of 'regex' with 'replacement'
sed 's/regex/replacement/'

The interesting part is that the regular expression given applies to the so called pattern space. Without any further modification the pattern space just holds the current line as read from the input. But sed has some tricks up its sleeves that allows for manipulation of the pattern space. For example the above command can be prefixed with an address. This address determines for which lines the command should be executed.

# Replace occurrence of 'regex' with 'replacement'
# only in lines 1 to 5
sed '1,5s/regex/replacement/'

The address itself isn’t constrained to only line numbers. We can use regular expressions here as well.

# Replace occurrence of 'regex' with 'replacement'
# only in lines matching the regex 'foo'
sed '/foo/s/regex/replacement/'

Well, OK, boring. Let’s get to the nitty gritty.

# Only consider lines matching 'self.bar'
# Append the following two lines to the pattern space
# Keep the same indent by matching whitespace
# Replace the 3 lines with our new method
# while keeping the indent
# `sed -i` applies the change in place.
sed -i '/self.bar/{N;N;s/^\([[:blank:]]*\).*/\1self.bar_baz_foo/}' foo.rb

Now if you combine this with find -exec or xargs you get a decent multi line search and replace work flow. If you think you need to dig in deeper don’t hesitate to join #sed on Freenode or look at the sed FAQ. Have fun!