Introduction to Thor

256px-Ruby_logo.svg

Looking at RubyGem’s list of “Most Downloaded” gems, we notice lots of familiar names: “rake”, “rails”, “rack”, “actionmailer”, etc. But, “thor” is an exception; few of us have heard of it and even fewer have written code with it. So, what exactly is Thor?

It is a way to write powerful command line utilities in Ruby. It makes argument parsing really easy and specifies a certain format for command line arguments. Tons of Ruby projects use Thor to make writing command line utilities (for example, the “rails” command) easy, quick, and fun.

But, in order to understand why Thor is so awesome, we have to first understand what kind of problem it solves.

The Olden Days

Back when dynamic languages were meant to be for those who didn’t understand pointers, we had a procedure called getopt_long to parse command line options. The code required to actually use this function was generally a large switch statement which, once written, was to be forgotten. Until, of course, the day came when another option had to be added. Check out an example:

while (1)
         {
           static struct option long_options[] =
             {
               /* These options set a flag. */
               {"verbose", no_argument,       &verbose_flag, 1},
               {"brief",   no_argument,       &verbose_flag, 0},
               /* These options don't set a flag.
                  We distinguish them by their indices. */
               {"add",     no_argument,       0, 'a'},
               {"append",  no_argument,       0, 'b'},
               {"delete",  required_argument, 0, 'd'},
               {"create",  required_argument, 0, 'c'},
               {"file",    required_argument, 0, 'f'},
               {0, 0, 0, 0}
             };
           /* getopt_long stores the option index here. */
           int option_index = 0;

       c = getopt_long (argc, argv, "abc:d:f:",
                        long_options, &option_index);

       /* Detect the end of the options. */
       if (c == -1)
         break;

       switch (c)
         {
         case 0:
           /* If this option set a flag, do nothing else now. */
           if (long_options[option_index].flag != 0)
             break;
           printf ("option %s", long_options[option_index].name);
           if (optarg)
             printf (" with arg %s", optarg);
           printf ("\n");
           break;

         case 'a':
           puts ("option -a\n");
           break;

         case 'b':
           puts ("option -b\n");
           break;

         case 'c':
           printf ("option -c with value `%s'\n", optarg);
           break;

         case 'd':
           printf ("option -d with value `%s'\n", optarg);
           break;

         case 'f':
           printf ("option -f with value `%s'\n", optarg);
           break;

         case '?':
           /* getopt_long already printed an error message. */
           break;

         default:
           abort ();
         }
     }

Even if you’ve never looked at C code in your life, it doesn’t look like a pleasant experience. So, they came up with libraries to make this operation a little bit simpler. In the Perl community, “coming of age” includes writing your own command-opts parser. As it turns out, Perl’s heavy usage in command line utilities grew from its enviable GetOpts::Long module. Check out this example from Perldoc (Perl’s documentation system):

use Getopt::Long;
my $data = "file.dat";
my $length = 24;
my $verbose;

GetOptions("length=i" => \$length, 
          "file=s" => \$data,
          "verbose" => \$verbose) or die("Error in command line arguments");

Basically, this code allows you to specify length, data, and verbose options, along with numerical, string and boolean values, respectively. But, this still doesn’t seem right. It doesn’t feel very clean or object oriented because we’re just stuffing things into variables.

Thor is the “Ruby way” of doing command line option parsing. It makes everything wonderfully object-oriented and abstracts the details away. This allows you to concentrate on your grand idea instead of messing around with options and string parsing unnecessarily.

Let’s see a concrete example of how Thor works its magic.

First Steps with Thor

Let’s get straight to a simple example:

require 'thor'

class SayHi < Thor
  desc "hi NAME", "say hello to NAME"

  def hi(name)
    puts "Hi #{name}!"
  end
end

SayHi.start(ARGV)

If you run this without any arguments, you should get something like this as output:

Commands:
  first_steps.rb help [COMMAND]  # Describe available commands or one specific command
  first_steps.rb hi NAME         # say hello to NAME

With only a few lines of code, we have a full description of the command we’ve created! Of course, if you run the script with the arguments “hi Dhaivat”, you should get a response back saying “Hi Dhaivat!”. Let’s break down the code.

We created a class called “SayHi” that derives from the Thor class. Then, the next line talks about describing a specific command. The first argument to the “desc” function is “hi NAME”, which describes what kind of a command we want. In this case, it says that we want the user to be able to type in “hi” and then their name, which will be passed in as a variable. Another example would be “location LATITUDE LONGITUDE”, where the user can have pass in “location 64.39 21.34″ and the location command will receive the latitude and longitude specified by those numbers.

What exactly do I mean by receive? Once we pass an argument, Thor figures out what kind of format it fits in and calls that method from our “SayHi” class. In this case, it calls the “hi” method with “name” as an argument.

Finally, we have our “hi” method which is just standard Ruby code.

Thor applications generally follow this sort of pattern; there are lots of features that can be used, but the general, underlying concepts remain the same.

Let’s write up a small utility, called file-op that has a command line option to output the contents of a file. Note: I’m not naming this utility “cat” because there are too many implementations of it in this world.

require 'thor'

class FileOp < Thor
  desc 'output FILE_NAME', 'print out the contents of FILE_NAME'

  def output(file_name)
    puts File.read(file_name)
  end
end

FileOp.start(ARGV)

It is quite nearly the same concept as the “SayHi” example, but this time if you run it with “output FILENAME”, it will output the contents of a file (which is handled through the “output” method of the “FileOp” class).

Digging a bit deeper

One of Thor’s most awesome features is the automatic “help generation”. The “desc” method used earlier has a second argument which actually describes what each command is designed to do. So, with our file-op utility, if run:

ruby file-op.rb help output

Thor prints out a nice usage notice:

Usage:
  file_op_v1.rb output FILE_NAME

print out the contents of FILE_NAME

Not only are we clearly documenting what each command does in our code, the user knows what each command does too!

Obviously, few significant commandline applications are going to be satisfied with such a basic options structure. Fortunately, Thor has our back there too. Wouldn’t it be nice if we could add a flag to our “output” command to output the file to stderr? I know, it wouldn’t really be all that nice, but let’s do it anyway:

require 'thor'

class FileOp < Thor
  desc 'output FILE_NAME', 'print out the contents of FILE_NAME'
  option :stderr, :type => :boolean

  def output(file_name)
    #options[:stderr] is either true or false depending
    #on whether or not --stderr was passed
    contents = File.read(file_name)
    if options[:stderr]
      $stderr.puts contents
    else
      $stdout.puts contents 
    end
  end
end

FileOp.start(ARGV)

We’ve added a few lines, but the most important is the option :stderr. This line tells Thor that whatever command we just defined (i.e. “output” in this case) can have a flag passed in as a boolean. In other words, it is either passed in or not passed in; it doesn’t have another value attached to it like --times 15 would. So, we can run:

ruby file-ops-v2.rb output --stderr filename

which would print the contents of “filename” to stderr.

But, this whole “option” thing makes Thor a bit odd. How does it know which command we’re adding the option to? Anytime you’re using option, it refers to the desc call just before it. Let’s add another command to our file-op utility that simply creates an empty file. This is equivalent to touch on *nix systems, so we’ll call the command “touch”:

require 'thor'

class FileOp < Thor
  desc 'output FILE_NAME', 'print out the contents of FILE_NAME'
  option :stderr, :type => :boolean
  def output(file_name)
    #options[:stderr] is either true or false depending
    #on whether or not --stderr was passed
    contents = File.read(file_name)
    if options[:stderr]
      $stderr.puts contents
    else
      $stdout.puts contents 
    end
  end

  desc 'touch FILE_NAME', 'creates an empty file named FILE_NAME'
  option :chmod, :type => :numeric
  def touch(file_name)
    f = File.new(file_name, "w")
    f.chmod(options[:chmod]) if options[:chmod]
  end
end

FileOp.start(ARGV)

The implementation is quite simple. All I’ve done is finished up with the output code and then placed the desc call for “touch”.

As an added bonus, I even included a --chmod option which allows you to set the permissions of the files you “touch” (e.g. passing in a 000 essentially creates a locked file). As a matter of personal preference, I don’t like the way Thor maintains the link between option and the method it affects, although I’m not sure of an alternative solution that would be as nice to use.

Class Options

What about commands that can be applied to any command? Something like:

some_utility.rb -v

Thor has us covered. These options, which are not associated with a specific command, are called class options. We could do with some more information when our utility runs. But, we usually don’t want this information, so we’ll include a verbosity option:

require 'thor'

class FileOp < Thor
  class_option :verbose, :type => :boolean
  desc 'output FILE_NAME', 'print out the contents of FILE_NAME'
  option :stderr, :type => :boolean
  def output(file_name)
    log("Starting to read file...")
    #options[:stderr] is either true or false depending
    #on whether or not --stderr was passed
    contents = File.read(file_name)
    log("File contents:")
    if options[:stderr]
      log("(in stderr)")
      $stderr.puts contents
    else
      log("(in stdout)")
      $stdout.puts contents 
    end
  end

  no_commands do 
    def log(str)
      puts str if options[:verbose]
    end
  end

  desc 'touch FILE_NAME', 'creates an empty file named FILE_NAME'
  option :chmod, :type => :numeric
  def touch(file_name)
    log("Touching file...")
    f = File.new(file_name, "w")
    f.chmod(options[:chmod]) if options[:chmod]
  end
end

FileOp.start(ARGV)

If you run this:

ruby file_op_v5.rb output some_file --verbose

You should see some log messages in addition to the file contents. Let’s take a look at the code.

We’ve added the log method, however, it is contained in a nocommands block. In order to tell Thor that log is not associated with a command we have to put it within this block. Within the log method, we print out the given string if Thor receives --verbose. We then use this method within the output and touch commands.

Wrapping It Up

Thor is an incredibly versatile library and makes command line parsing easy and intuitive. However, there are a few hiccups such as the nocommands block that one should be aware of to avoid potential gotchas.

I’ve used Thor in many different places, many times for short utilities to move some files around or search through some data. I have found it a very useful tool and hopefully you will, too.

The source for this article can be found here.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • James Conroy-Finn

    There are some nice examples of Thor’s CLI functionality on the [homepage][].

    Thor was built as a replacement for Rake, and as such offers some methods you’d expect to find in a task-based build tool.

    Perhaps a post on the contents of [`Thor::Actions`][] would be a nice follow up post.

    Thanks for sharing what Thor can do to help in any Ruby application and library.

    [homepage]: http://whatisthor.com
    [`Thor::Actions`]: http://rdoc.info/github/wycats/thor/Thor/Actions

  • Anonymous

    What a great article. It is nice to see there are developers concerned about all sort of things in the Ruby language, not only web apps. After all, Ruby is an exceptional language not only for the Web.

    • Dhaivat Pandya

      Thanks, Guilherme! I’ve definitely used Ruby for a *lot* more than web apps.

  • http://enria.org/ davetron5000

    Thor is good to know, as it comes with rails (this is why it appears to be so popular), if anyone’s interested in other command-line app frameworks, I’m the author of GLI and did a quick write-up here: http://www.naildrivin5.com/blog/2013/12/02/introduction-to-gli.html

    I also maintain a list of all Ruby command-line gems here: http://www.awesomecommandlineapps.com/gems.html