Writing Awesome CLI Tools in Ruby: Part I

by

Introduction

I am always writing small tools to help me out on a daily basis. Sometimes shell scripts, but
other times I want something a bit more complex. When I need more than a simple shell script, I like to leverage ruby for its vast library of gems which can greatly accelerate and simplify the task of building these helpful tools.

This post will give an introduction to writing your own CLI tools in ruby and packaging them
as a gem. There are many concepts that I want to cover here, but that would prove to be a very lengthy post. To simplify and make this concept more easy to digest, this post is the first of a short series of posts.

Benefits to building gem packaged utilities

  • Gems can install to your $PATH, allowing you to run your utility from anywhere
  • We can leverage a giant library of existing gems to simplify and accelerate
    development
  • Sharing/distributing your tools becomes extremely easy.
  • When properly designed, a CLI tool can also function as a reusable library for future
    projects.

Example application

This post will walk you through a simple example that I threw together. The example utility is a tool to query wunderground from your command line called “wunder“.

If you wish to follow along and use this example utility, you will need an API token for wunderground.

Getting Started

Ensure that bundler is installed. If not, $ gem install
bundler
(you may need sudo if you cannot install gems locally).

Once you have bundler installed, execute $ bundle gem mytool, which creates your
basic project structure for a gem. For my example, wunder, structure looks like this:


wunder/Gemfile 
wunder/Rakefile
wunder/LICENSE.txt 
wunder/README.md 
wunder/.gitignore 
wunder/wunder.gemspec 
wunder/lib/wunder.rb 
wunder/lib/wunder/version.rb

Organization

You should put all of your source files in lib/ within modules named for your
gem. Looking to the example, you will notice that everything within lib/ is inside
of the Wunder module. This keeps everything separate and organized for future use, and does not pollute your ruby’s load path.

Executables

The whole point of building a CLI tool is to have an executable command in your path once it is installed. My preferred approach is to build a single executable per gem that takes
subcommands as arguments. That executable is placed in bin/ and is executable.

In our example, wunder-rb/bin/wunder is the executable. To make your script executable run

$ chmod +x bin/myexecutable.

The gemspec (wunder.gemspec) declares files in bin/ to be executable with this line:

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

Packaging your gem

Bundler provides some rake tasks to help you package, install, and even publish your gem.

  • rake build – build and package your gem into
    pkg/mygem-version.gem
  • rake install – build and install your gem to your local system
  • rake release – publish your gem on rubygems.org

While developing you’ll want to be testing your code manually. In order to run your gem
without building and installing it, you can run it with bundle exec bin/myapp and
your executable will be run in the context of your defined environment in the
Gemfile/gemspec.

Next steps

This post leaves you at a place where you are able to create a gem project structure, add an executable to the bin/ directory, and package/install your gem. If you’ve done everything correctly, you will be able to run your installed gem by simply executing the command that corresponds with the name of your executable in bin/. In our case, $
wunder
works.

You will notice that the example project has a lot more things going on. I will cover each of
those things in Part II of Writing Awesome CLI Tools in Ruby

Some topics to be covered:

  • Designing your utility to function both as a library and a CLI tool
  • Gems to make your life easy when writing these types of utilities
  • Configuration files
  • Rich commandline api’s

Leave a Reply

Your email address will not be published. Required fields are marked *