A simple Ruby command-line tool

In some cases you might need a simple command-line tool. But one of the problems writing those scripts is finding a descent way to manage and distribute them. One way is to bundle your command-line tool into a ruby gem.

I already described in a previous post how to create a ruby gem, so I won’t be covering that part again.

Executable

You can’t have a command-line tool without an executable. Adding an executable to a gem is a simple process. Just place a file in your gem’s bin directory, make sure the spec.executable directive is present in your .gemspec and you are good to go:

...
spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
...

Lets create your executable in the bin/ directory and name is my_gem. It might look like this:

#!/usr/bin/env ruby

puts 'My executable works!'

The shebang tells the system that is should use Ruby to execute our code. The only thing that happens now is that a sentence gets written to the screen.

You can test this by simply executing the script from command-line:

$ ./bin/my_gem
My executable works!

Thor

Of course, if you want to develop a command-line tool, your tool must be controllable. You’ll need to execute multiple commands and each command might take multiple options and arguments. There are several tools out there that can help you making the task less tedious. One of the tools and the one I’m going to use is Thor. Thor lets you create a command-suite app simply and easily, as well as Rails generators. Thor will help you to script your command actions and command-lines.

It will remove the pain of having to handle command-line options parsing, documentation and task dependencies. Thor can also be used as an alternative to the Rake build tool. The syntax is Rake-like, so it should be familiar to most rake users.

So to get started, add the Thor gem to your .gemspec file:

...
spec.add_dependency 'thor', '~> 0.18'
...

and run bundle to install the gem:

$ bundle update

Now lets create an application class that will handle our first command. I personally prefer adding Cli classes in a separate namespace so it doesn’t clutter the business intelligence. Create an Application class in lib/my_gem/cli/application.rb :

require 'thor'

module MyGem
 module Cli
   class Application < Thor

     desc 'hello NAME', 'Display greeting with given NAME'
     def hello(name)
       puts "Hello #{name}"
     end
   end
 end
end

First you’ll need to require ‘thor’ and create your Application class that inherits from the Thor class. Then you start of by giving the command and command description. Here we have a command hello that takes a parameter name. The only thing this command do is display the given name.

Your cli still won’t work since your executable has no clue of the cli interface. Require the application class file in your gem lib/my_gem.rb

#!/usr/bin/env ruby

require 'my_gem'

MyGem::Cli::Application.start(ARGV)

If you would run your executable now, you would get an error in the form of:

$ ./bin/my_gem
kernel_require.rb:45 in `require': cannot load such file -- my_gem (LoadError)
...

This error is caused by the fact that your environment is not loaded yet, so you lib/ directory is not present in the load path and the my_gem.rb file cannot be found. This only happens during development because you run the executable directly.

Some developers solve this by manipulating the load path via $: or $LOAD_PATH, but I consider this bad form. Since we are using bundler, it’s easiest to just run your app locally:

$ bundle exec bin/my_gem

This way, bundler will include the lib directory in its load path before running the executable.
If the arguments you pass to start are empty, Thor will print out a help listing for your class:

$ bundle exec bin/my_gem
Commands:
  my_gem hello NAME       # Display greeting with given NAME

Thor also makes it easy to specify options and flags as metadata about a Thor command, but I’m not taking this into detail. There is one more points though, I wish to handle.

Subcommands

As your CLI starts to grow, you might want to be able to specify a command that points at its own set of subcommands. Lets say we would like to expand our executable with a build command for different output formats. We could do this using a build command that takes a type parameter. But what if other parameters are possible, but the other parameters differ from the type?

This is where subcommands might come in handy. To add our build subcommands, add the following lines to your application.rb class:

...
desc 'build TYPE', 'Create a new build. Type can be html, epub, pdf'
subcommand 'build', MyGem::Cli::Build
...

Here we tell Thor that the Build class holds subcommands for the general build command. Now create the lib/my_gem/cli/build.rb file and create the Build class:

require 'thor'

module Pressman
 module Cli
   class Build < Thor

     desc 'html', 'Build html output'
     def html
       ...
     end

     desc 'pdf', 'Build html output'
     def pdf
       ...
     end

     desc 'epub', 'Build html output'
     def epub
       ...
     end

     desc 'mobi', 'Build mobi output'
     def mobi
       ...
     end
   end
 end
end

and require the build file in your gem ( lib/my_gem.rb )

...
require '>my_gem/cli/build'
...

Now if you run the build command without parameters, you will see a list of all available subcommands:

$ bundle exec bin/my_gem build
Commands:
  my_gem build epub            # Build html output
  my_gem build help [COMMAND]  # Describe one or more subcommand(s)
  my_gem build html            # Build html output
  my_gem build mobi            # Build mobi output
  my_gem build pdf             # Build html output

As you can see, Thor makes it easy to write cli executables and in a very clean way if I might add. Of course there are more ways to write a command-line tool. The only thing I can recommend is to use the one that fits your needs best. Some command suite tools beside Thor: