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 [ ] = { { "verbose" , no_argument , & verbose_flag , 1 } , { "brief" , no_argument , & verbose_flag , 0 } , { "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 } } ; int option_index = 0 ; c = getopt_long ( argc , argv , "abc:d:f:" , long_options , & option_index ) ; if ( c == - 1 ) break ; switch ( c ) { case 0 : if ( long_options [ option_index ] . flag != 0 ) break ; printf ( "option %s" , long_options [ option_index ] . name ) ; if ( optarg ) printf ( " with arg %s" , optarg ) ; printf ( "

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

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

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

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

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

" , optarg ) ; break ; case '?' : 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 ] first_steps.rb hi 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 ) 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 ) 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..." ) 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.