Yehuda Katz is a member of the Ember.js, Ruby on Rails and jQuery Core Teams; he spends his daytime hours at the startup he founded, Tilde Inc.. Yehuda is co-author of best-selling jQuery in Action and Rails 3 in Action. He spends most of his time hacking on open source—his main projects, like Thor, Handlebars and Janus—or traveling the world doing evangelism work. He can be found on Twitter as @wycats and on Github.

By Thor’s Hammer!

For the past few months, I’ve become more and more disillusioned by the current state of Ruby’s scripting support. Sure, we have optparse, and a gamut of other solutions, but there’s no full-stack package for writing robust binaries.

Enter thor.

The idea behind thor initially came from my work on a textmate binary (more on that later today), which would manage installed Textmate bundles. Sure, there’s the getbundle bundle, and you can manually get bundles from subversion, but I wanted a single binary that handled all of that.

While I was building it, I decided to map the commands that people could enter to a class and its methods. I created a small file that I included with the textmate binary called class_cli, and created some syntax for mapping things, like so (note that this is not the final syntax):

class MyApp
  include CLI

  desc "list [list]", "list some items"
  def list(list = "stuff")
    puts list.split(/,\s*/).join("\n")
  end
end

MyApp.start

Assuming the binary is called app, you could then do app list "one,two,three", which would print:

one
two
three

In the case of the textmate binary, I created methods for list, install, installed, and uninstall.

This worked nicely, so I wanted to extract it out for use in other tools. I started the Hermes project on github, but while drinking with Chris Wanstrath, I was convinced that the name Thor would be more appealing. Oh, the things that alcohol can do. I was also convinced of a couple other things (again, in a slightly inebriated state):

  • rake and sake needed to be replaced for scripts, not as a replacement for make
  • nobody was going to use hermesthor if they need extra boilerplate like MyApp.start

Long story short, Chris convinced me to make Thor a full-fledged scripting solution. So I did.

Thor, as it exists today, has two components:

  • The Thor superclass, which works exactly like CLI/Hermes, except that you inherit from it instead of including it (class MyApp < Thor).
  • The Thor runner, which can run Thortasks that are in a local directory or installed from a remote location

The thor runner allows you to make files like:

# module: random

class Amazing < Thor
  desc "describe NAME", "say that someone is amazing"
  method_options :forcefully => :boolean
  def describe(name, opts)
    ret = "#{name} is amazing"
    puts opts["forcefully"] ? ret.upcase : ret
  end
  
  desc "hello", "say hello"
  def hello
    puts "Hello"
  end
end

If you call the file *.thor or Thorfile and place it in your current directory, any directory above you, or tasks/*.thor, you can then invoke the thorfile in any of the following ways:

$ thor -T
Tasks
-----
amazing:describe NAME [--forcefully]   say that someone is amazing
amazing:hello                          say hello

$ thor amazing:hello
Hello

$ thor amazing:describe "This blog reader"
This blog reader is amazing

$ thor amazing:describe "This blog reader" --forcefully
THIS BLOG READER IS AMAZING

You can also install local tasks or remote tasks to your system thor cache and make them available anywhere:

$ thor install task.thor
Your Thorfile contains: 
# module: random

class Amazing < Thor
  desc "describe NAME", "say that someone is amazing"
  method_options :forcefully => :boolean
  def describe(name, opts)
    ret = "#{name} is amazing"
    puts opts["forcefully"] ? ret.upcase : ret
  end
  
  desc "hello", "say hello"
  def hello
    puts "Hello"
  end
end
Do you wish to continue [y/N]? y
Storing thor file in your system repository

$ thor installed
Name      Modules
----      -------
random    amazing

Tasks
-----
amazing:describe NAME [--forcefully]   say that someone is amazing
amazing:hello                          say hello

$ thor amazing:hello
Hello

... same as above ...

You can also specify a URL instead of a file name; like sake, thor uses open-uri to get the files. You uninstall or update thor modules based on the short name that was provided; if # module: name exists at the top of the file, thor will use that by default. You can also use thor install task.thor --as my_short_name. If you don’t provide a short name, thor will ask for one.

Later, you can do thor update short_name and thor will remember where you got the module from and try to update it. thor uninstall short_name will remove the module from your list of installed modules.

Of course, thor -T (or thor list) will list local tasks and system-wide tasks in the resulting list, so you don’t need a separate tool to track your thortasks, and a local task can be made into a system task trivially.

Finally, thor itself is self-hosting; the thor runner uses the Thor superclass. As a result, I added some more features to the superclass as I built the runner:

  • You can map short names to their full name: map "-T" => :list, which is why thor -T and thor list are identical
  • You can provide additional options that get passed in as a Hash (method_options :as => :required). The available option types are :required, :optional, and :boolean. The resulting hash is passed in as the final parameter to your method and the pretty-printed help automatically includes them in the usage screen.

Take a look at the thor repository on github, and specifically, the thor runner for more information on how it all fits together.

31 Responses to “By Thor’s Hammer!”

How does this actually differ from rake, other than by not having dependencies?

The primary difference is how you specify tasks and command-line options. With rake, you get a bunch of free-floating tasks with little hacked on support for basic command-line options.

Thor gives you structure, plus an excellent mapper for command-line options to the methods in question. Think of it like a Rails or Merb controller for your command-line scripts.

Also, Thor has built-in support for system-installable scripts, including ones hosted remotely. So it has sake built right in.

I guess I’m thinking the same thing. I definitely agree that specifying command-line arguments could be nicer than Rake currently allows. It’d be nice to feel like I didn’t have to choose between Thor and Rake, however.

Great idea, but what about MVC for the command line?

SimpleConsole did that:

http://simpleconsole.rubyforge.org/

Better encapsulation :-)

But needed more love, maybe a router, and a console mode…

This is definitely nice!

I’m all about writing scripts around my system to better maintain the files and things I have installed. I’ve been using Sake for sometime, but this make its much nicer.

Any chance of building in support for RSpec?

I tend to leave small spec files in the same directories as my scripts, but if I could just write them at the top of my Thor scripts, and then have them run quickly everytime I run the script that would be lovely.

Of course, I probably wouldn’t want that for larger scripts.

This looks very nice. One immediate advantaged I see over rake is that its plain old Ruby, so you can test the ‘tasks’ just as easily as anything else. With any substantial rake tasks I end up pulling out all the logic to class methods to make it testable, which always feel a little dirty. This would be a nice way around that.

Looks pretty cool!
I’ll be using it on the next script I need to work on. Thanks!

@Justin: The latest release of thor has built-in support for rspec, and the Thor project itself is now self-hosted. Simply add:

spec_task(Dir["spec/**/*_spec.rb"])

to any of your Thor classes. You can also customize it like so:

spec_task(Dir["spec/**/*_spec.rb"], :name => “rcov”, :rcov => {:exclude => %w(spec /Library /Users task.thor lib/getopt.rb)})

You can also add package/install tasks via: package_task / install_task (where install_task adds package_task by default).

My initial reaction was… Why? But upon further thinking and browsing the source code, this is pretty cool stuff.

I still don’t get it. Why not patch rake to take command-line arguments in a sane way?

this is awesome! I’ve been using simpleconsole for a bit, but this looks really nice too!

what would be REALLY nice is something like this or simpleconsole for n/curses!

Heyyy, like the `procore` script I wrote; except more frilly things. :D

@Colin heh I didn’t even remember that script. Very similar concept (minus the runner and option parsing). Maybe rewrite `procore` on top of Thor? Should be pretty trivial plus you’ll get pretty option mapping.

Hi, thanks for the script, I like it very much, just a few things:
- it swallows some exceptions and says that task not found
- are there any way to detect that the script will not be executed just a task list is being generated? (for like skip the require section for task list)
- Thor uses the class’s constructor to execute the call, it would be nice to have an overrideable init function for the class (parameters are not needed, it’s a global thing). This can help to avoid using class variables which are initialized even for a thor -T.

@teki
“it swallows some exceptions and says that task not found” — this is resolved on trunk. I’ll push a new release tonight or tomorrow.
“are there any way to detect that the script will not be executed just a task list is being generated? (for like skip the require section for task list)” — I’m not sure what you’re asking
“Thor uses the class’s constructor to execute the call, it would be nice to have an overrideable init function for the class (parameters are not needed, it’s a global thing). This can help to avoid using class variables which are initialized even for a thor -T” — Yep. We’re working on this at the moment. You’ll even HAVE global parameters :) This will probably be in 0.9.3 (the next release is 0.9.2).

I have this now to avoid loading in the modules:
http://rafb.net/p/izKfMc88.html

A small change for windows users:
break if path == “/” || /\w:\// =~ path

(in bin/thor or in lib/thor/runner.rb)

Hi Yehuda,

I downloaded thor from GitHub but unfortunately I have no clue how to install it or where to put the files.

Could you please help me on this one?

Thanks in advance

Ok, I overlooked the gemspec file so now I created a gem and installed it. Works fine!

Thanks

I’ve been working on a gem (tap) with similar goals as thor. I just got asked to compare/contrast:

http://bahuvrihi.wordpress.com/2008/08/08/distributable-tasks-and-workflows/

I was wondering if you’d like to fact check and/or do something similar?

You guys might want to check out the Commander gem, for executables.

http://github.com/visionmedia/commander

I created a small Thor file that helps to maintain a Ruby gem (producing a gemspec, packaging and releasing to RubyForge). I called it… Joe :-)

http://github.com/djanowski/joe

thor install http://dimaion.com/joe/joe.thor

Thor rocks!

Error in my previous message. What I typped at the command line was:

thor amazing:describe NAME [--forcefully]

instead of just:
amazing:describe NAME [--forcefully]

Leave a Reply

Archives

Categories

Meta