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 -Tandthor listare 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.
Comments(13)