Using Thor to Build a Command Line Interface

My first experience with Thor wasn’t great. I had heard that it could be used as a Rake replacement. Now, I have no problem with Rake (I like Rake!), but I’m always open to trying something new. I didn’t like the new thing.

Well, I didn’t like it until I tried to use Thor for what it claims to be good at (go figure). In fact, I much prefer Thor to something like Ruby’s OptionParser and other similar libraries. I can’t put my finger on exactly why, but I find command line interfaces I create with Thor much more aesthetically pleasing than other comparable libraries.

1. Starting Out

We’re going to build a simple command line interface around a simple library that provides grep-like functionality around Ruby’s regular expression functionality. This library is called Tools. Let’s call the command line interface object ToolsCLI.

First things first, we need to define a class that inherits from the Thor class and instruct our script to use this class as a Thor program. This is easy:

require 'thor'
class ToolsApp < Thor
end
ToolsApp.start

2. Adding a Command

Next, we need to add a command. Our library, for now, provides just a single command. It’s creatively called grep. It accepts a file and a pattern as arguments and prints a list of lines from the file that match the pattern.

We add this command by adding the following lines to our ToolsApp class:

desc "grep [file] [pattern]", "Print lines from [file] matching [pattern]"
def grep(file, pattern)
end

3. Exploring Thor

At this point, we can run our program and see what Thor gives us:

$ ./toolsapp.rb
Tasks:
  tools grep [file] [pattern]  # Print lines from [file] matching [pattern]
  tools help [TASK]            # Describe available tasks or one specific task

Here we see that Thor has taken our task and used the description provided to document how the command works. We also notice that it’s kindly given us a help task. We can invoke this to get more specific information about a given task.

$ ./toolsapp.rb help grep
Usage:
  tools grep [file] [pattern]

Print lines from [file] matching [pattern]

4. Adding an Option

This is kinda boring; it looks just like what we had before. Let’s add an option. Our new option will invert the logic of the grep command. Instead of printing lines that match the pattern, let’s add an option that prints the lines that don’t match.

To do this, we need to change our grep definition to the following:

desc "grep [file] [pattern]", "Print lines from [file] matching [pattern]"
method_option :invert, type: :boolean,
                       default: false,
                       desc: "Only print lines that don't match pattern."
def grep(file, pattern)
end

When we ask for the help information again, we’ll see the new information about the invert option we’ve just defined.

$ ./toolsapp.rb help grep
Usage:
  tools grep [file] [pattern]

Options:
  [--invert] # Only print lines that don't match pattern.

Print lines from [file] matching [pattern]

There we go. We now have a script that uses Thor to produce a nice, easy command line interface to our program.

5. Calling into Our Program

Now we just need to wire in our calls to the actual library. For organizational purposes, I’ve defined the actual Tools class in a different file. Let’s add the call to the grep function.

desc "grep [file] [pattern]", "Print lines from [file] matching [pattern]"
method_option :invert, type: :boolean,
                       default: false,
                       desc: "Only print lines that don't match pattern."
def grep(file, pattern)
  Tools.grep(file, pattern, options)
end

Method options are available through the options method. Here, we just pass the options down to the grep command for further processing.

That’s it. Let’s run the grep method on some Lipsum data with our Thor command line interface.

$ ./toolsapp.rb grep sample.txt lorem
  6: ligula eleifend sapien, eu sodales ligula velit eget lorem. Sed consequat
 16: molestie ac consequat id, suscipit nec lorem. Lorem ipsum dolor sit amet,
 34: Donec viverra lorem sit amet velit bibendum vel tristique urna dignissim.
 43: pharetra. Suspendisse luctus adipiscing massa, non porttitor lorem porta sed.

…and inverted…

$ ./toolsapp.rb grep --invert sample.txt lorem
  0: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
  1: sollicitudin erat sed justo elementum nec dictum nibh placerat. Integer
  2: scelerisque tortor nec dui hendrerit nec ullamcorper quam gravida. In sit amet
  3: turpis erat, non rhoncus nisi. Aliquam erat volutpat. Nulla ac ligula purus, et
  4: aliquet dui. Ut nec vestibulum nisl. Morbi malesuada arcu nec sapien pulvinar
  5: non ultrices orci tincidunt. Sed eleifend, urna non gravida porttitor, elit
  [notice line 6 is missing]
  7: convallis dolor sed porttitor. In at nibh ac elit mattis varius.
  [continued]

6. Class Options

Sometimes, one needs an option that is valid on every method. It would be silly to have to define the same option over and over again on every method. That’s why Thor provides a class option. Since looking through this sort of data can get rather boring after a while, I took the liberty of adding an option to spruce up our output a bit.

Class options are defined with the class_option method. These options can be added anywhere in your class, but it’s probably best to concentrate them at the top.

Here, I define our option:

class_option :moar_lols, type: :boolean,
                         default: false,
                         desc: "Turn on moar lols."

This option will be passed to the grep method in the same way that the invert option is. If this option is set to true, the tool will run its output through the lolcat library instead of printing it diretly to the screen. Here’s what our output should look like now:

Well, that was easy.

I’ve made the code for this project and some sample data available in a gist. I hope you learned something and will enjoy Thor as much as I have.
 

Conversation
  • aussielunix says:

    for two excellent ruby CLI libs checkout GLI and methadone. Both by the same author and a pleasure to work with.

  • […] of creating command line interfaces around Haskell applications. It takes about as much effort as Ruby’s Thor library does to create something internally useful to your program and pleasing to the […]

  • Comments are closed.