Thor is not new; first built as a
sake replacement, first commit is well over 4 years ago.
Recently, @wykatz emerged a fantastic looking (and much deserved) Thor website, and although I’ve started doing Thor based projects over two years ago, I think its the right time to write about Thor itself.
Today, Thor can serve as a rake replacement, great generator building framework, and a general purpose CLI toolkit.
Every once in a while, I tend to re-evaluate the things that form my “engineering toolbelt”. One of those is Thor, so I set out to undermine my assumptions and find something that can compete with it in terms of development ease of use.
This framework marks as ‘most popular’ in ruby library sites. However, as I’ve tried figuring out whats there to use, I came up short with things like developer convenience, interactive input (menus) and colored output.
I closed it up by reasoning that since the Chef
knife tool depends on it, the high rank it gets is probably biased by the incredible number of implicit downloads.
Having over 250 dependent gems, trollop looks to be the most popular framework around. It deserves a discussion of its own, it is minimal and simple, and definitely is my pick when building one-off CLI tools.
It is similar to trollop in simplicity, I’ve used it before and its also a great one for small CLI tools.
Commander goes a step further and gives you a nice DSL to work with, it has a similar version for Node.js which is fun to use (by same author). Having built a tool with the Node.js version, I haven’t found it completely in advantage over simpler toolkits such as slop and trollop.
Finding the right match
So initially, I picked up trollop, and set out to build a tool that will have:
- Clear separation of concern
- Interactivity (input, menus)
- Aesthetics (commands, colors)
The first hurdle was designing an aesthetic CLI (yes, CLI has the notion of design, for some people :). It so happens that a command would have parameters and options and that options are dasherized (
--opt-name=<VAL>) and that whole concoction doesn’t really look well when you write the code to take in:
For example, working out how I’m going to test my tool with each of these (are they expecting to be tested in any special way? is there a preliminary setup?) wasn’t a good use of my time for such a small project in my opinion.
Faces of Thor
Although Thor is a single tool, in practice, it can be used in several ways (that I’ve so far had experience with):
test.thor lives in your directory This will result in you being able to say:
An example tool you’d like to build can be a project sekeleton generator. Given a few command line parameters, It’ll generate a best-practice folder layout, with boilerplate code living inside generated files. The most obvious example would be the Rails generator with
Most (if not all) of the things you see that the Rails generator does are exposed to you through Thor. Here’s an example:
And here is a typical template making use of the context we set up in the Thor task
Although these are very exciting, I’d like to focus on Thor being a command line toolkit.
Thor: a CLI framework
The minimal CLI app:
In a CLI app context, you would inherit from
Thor, which makes available
desc and other primitives for you to base your tool on.
Thor maps the CLI onto a class.
If you look closely, one of the ka-ching moments of mine with thor is that it maps:
to the familiar:
while options are specified as:
and invoke as:
Easily enough, in code, you would use the
options variable, available at the body of the action.
Building a tool
Lets ditch out the idiotic earth-shaking example and use Logbook to work out the details. For any of the points below referencing CLI code, feel free to point a browser tab at the actual code to get more context, its around 100LOC.
I’d like to sketch out my CLI tools as such:
Where the main points are: a near-empty shim in
bin/shake containing something along the lines of:
cli.rb module containing all of the command-line specific mechanics; and a set of modules (here
book.rb) that contain the domain logic (in the domain-driven-design sense).
The CLI module should only bridge between the ‘ugly’ world of tty I/O and the beautiful, clean, world of our domain layer. The immediate gain is with testing, of course.
To interact with the user, Thor provides several primitives such as
print_table, here is an example usage:
As mentioned before, Thor will auto-map the commandline into your class, so this allows me to take something like:
I wanted to use color, so I’ve started out with
rainbow mentioned before. But knowing Thor already does this with the Rails generator, I quickly found:
and the more low level shell abstraction
To set a text to “bold” (or “bright”) without really modifying its color (which apparently was not supported in
Addmittedly, this is something Thor does not provide. I needed to save state about the user for the application, the same way the
heroku gem saves your login information, for example.
This means storing a
.dotfolder or a
.dotfile in your
~/.config. While I could do this easily, I found user_config to be minimal and excellent.
Dealing with commandline apps I/O, this might (and rightly so) intuitively sound frightening; so it might not be very obvious how to tackle it. Although aruba might cover it, it is best to design your tool in a testable manner.
Given that we’ve encapsulated most of our logic outside of the Thor CLI class, it should be very easy to test. The book module hands out all of Logbook’s functionality.
In this specific case it is exposed by CLI, in another imaginary case it could be exposed to a Web service easily enough.
Given that we already know how to test modules separately, we’ll only need to test the CLI layer with logic mocked out, for interactions with the user and state.
Lets simulate a command and assert on output:
minitest does great with providing us
capture_io which fetches
STDOUT for us to match on. As you might have noticed, invoking a Thor action is also pretty easy.
Testing input is a different matter. Here, I’ve chosen to mock Thor’s
yes? primitive and precook it with values:
This will cause the flow to proceed as if the user typed in ‘yes’. I do the same where it is also a free input with
Thor will spit warnings when tasks are declared without
desc annotations. This most commonly will happen when you’re mocking things (in my case, I use
In addition, many times a CLI apps deals with filesystems/files, a good solution for testing would be
You can take a look at my monkey patching in logbook’s spec_helper
Thor gives you an almost brainless mapping from CLI to your code, arguably without using a specialized DSL, or requiring you to write additional boilerplate validation code after you’ve used a framework that takes care of arguments.
It will give you an awesome mileage for what you’re trying to do, and it is very worthwhile investing the time to specialize in everything it has to offer.