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 whythor -T
andthor 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.