Creating a Ruby Gem From a Thor Applicaton

A couple months ago I started to think about all the repetitive tasks I would do from the command line at work. I was introduced to Rake a while back. It was beautiful, simple and efficient.

There were a couple issues I had, though. I didn't like how Rake handled arguments. I didn't want to type rake -g every time I wanted to run a global task. There was also some issue with paths that I can't remember off the top of my head. All I remember was frustration. I didn't want a Rakefile littering all of my directories. I wanted a solution that was global and mature. Enter Thor.

So I started with a bunch of .thor files with tasks I deemed necessary. All was good. All was peachy. Then I was thinking about sharing my toolset with other co-workers. Sure, they could thor install all of my Thor tasks, but I wanted it to be easier than that. Making my Thor tasks into a gem was the solution. Here we go...

Problem

I want to create a gem from a Thor application.

Solution

First we need to create the gem. As far as this post in concerned we'll name it foo. Original, I know. Run the commands below in the terminal:

~ $ gem install bundler
~ $ bundle gem foo
      create  foo/Gemfile
      create  foo/Rakefile
      create  foo/.gitignore
      create  foo/foo.gemspec
      create  foo/lib/foo.rb
      create  foo/lib/foo/version.rb
Initializating git repo in /Users/cparaiso/blah/foo  
~ $ cd foo

Take a moment and look what the bundler gem created for us. The first thing we need to edit is our gemspec file and change some of the defaults. The properties s.summary and s.description need to be filled in with whatever you want. You're required to change these properties to something different or else the gem will not install on your local system. We also add a dependency for the Thor gem on line 24.

# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "foo/version"

Gem::Specification.new do |s|  
  s.name        = "foo"
  s.version     = Foo::VERSION
  s.authors     = ["Chris Paraiso"]
  s.email       = ["centroscape@gmail.com"]
  s.homepage    = ""
  s.summary     = %q{chrisparai.so gem tutorial}
  s.description = %q{description goes here}

  s.rubyforge_project = "foo"

  s.files         = `git ls-files`.split("\n")
  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  s.require_paths = ["lib"]

  # specify any dependencies here; for example:
  # s.add_development_dependency "rspec"
  # s.add_runtime_dependency "rest-client"
  s.add_runtime_dependency "thor" # add dependency for Thor
end

In the end when we install our gem, whatever is in the bin directory of the gem folder will be installed on our $PATH, so to speak. If you've noticed, the foo/bin directory does not exist. We'll need to create this directory and within that directory, create a file with the same name as your gem. In this case, it's foo. The directory structure should look like this: ~/foo/bin/foo.

~/foo $ mkdir bin
~/foo $ touch bin/foo
~/foo $ chmod +x bin/foo # make foo executable
~/foo $ git add *

Concept

The meat of our application should live in the lib directory of our gem. When creating a typical Thor application, we usually create a file with a .thor extension and run a thor install <name_of_thorfile.thor> to install it on our path. To convert said Thor application to a gem, those .thor files need to be renamed to .rb files and moved to our ~/foo/lib/foo directory. We can use bin/foo executable file to pull in all our Thor tasks that reside in lib/. More on that later.

Our lib/foo directory

In this directory we have the guts of our application. All your .thor files should have been renamed to files with a .rb extension and put in this directory. We can create a main.rb file in lib/foo to pull in all of our classes that inherit from Thor here. Let's take a look at main.rb:

#!/usr/bin/env ruby
require_relative "bar"# ~/foo/lib/foo/bar.rb  
require_relative "baz"# ~/foo/lib/foo/baz.rb  
module Foo  
  def self.const_missing(c)
    Object.const_get(c)
  end
end  

main.rb pulls all of its adjacent files (bar.rb and baz.rb) into a module we have named Foo. This can be named whatever you want it to. The source for these other files are just simple Thor task classes as shown below:

#!/usr/bin/env ruby
class Bar < Thor  
  desc 'beer', 'Description here.'
  def beer
    puts 'beer called from class Bar'
  end
end  
{% endcodeblock %}

{% codeblock ~/foo/lib/foo/baz.rb %}
#!/usr/bin/env ruby
class Baz < Thor  
  desc 'spaz', 'Description here.'
  def boo
    puts 'spaz called from class Baz'
  end
end  

That's our entire lib/foo directory. Mind you, it's dead simple for the sake of this post. In a project I'm working on, I have a util folder that holds methods and structures shared by most of my Thor subclasses. Let your imagination run wild. As long as we pull all our Thor subclasses in our Foo module (main.rb), we're all good.

All together now

The last step is to bring it all together in a file our gem will install so we can run it anywhere on the command line. We created this file near the beginning of the post.

#!/usr/bin/env ruby
require "thor"  
require "thor/group"  
require "foo/main"

class CLI < Thor  
  register(Foo::Bar, 'bar', 'bar <subcommand>', 'Commands for bar.')
  register(Foo::Baz, 'baz', 'baz <subcommand>', 'Commands for baz.')
end

CLI.start  

We, first, require what we need in order for this executable to be work with Thor. The call, require "foo/main" on line 3, is requiring ~/foo/lib/foo/main.rb. RubyGems take care of relative paths so we don't need to mess with all that mumbo jumbo. Any call to require will look in the gem's lib/. foo/main is the Foo module we created that pulls in classes from it's adjacent files (those files you renamed from .thor to .rb). We use the Foo module to register tasks in our main Thor application called CLI and call its start method with CLI.start to make ~/foo/bin/foo an executable Thor application.

Conclusion

That's it! Install our gem by running rake install from the gem's root (~/foo). Then run the tasks we've created!

 ~ $ foo
Tasks:  
  foo bar <subcommand>  # Commands for bar.
  foo baz <subcommand>  # Commands for baz.
  foo help [TASK]       # Describe available tasks or one specific task

~ $ foo bar beer
beer called from class Bar  
~ $ foo baz boo
spaz called from class Baz  

This is how I've created a gem from a Thor application. Of course, if anyone has a better/simpler way, please let me know.