Playing Around with Thor Generators
Mar 8, 2014Automating things has been somewhat of a life goal for me. Ultimately, I’m a pretty lazy person, and I hate to have to do things repetitively, and if I can get the computer to do it for me, I’ll write a script. (Although I admit this sometimes devolves into Yak shaving.)
so what is a generator?
Simply put, a generator is something that builds something else for further use. Sometimes this is referred to as a factory if you’re doing it in code. But here, I’m taking the meaning of creating files and directories, with possibly customized content, that will be used to frame in a bit of a coding project.
Thor is a nifty tool that was extracted from Rails to be a stand-alone tool. Generators are tools that build other things, typically used in Rails for models, controllers, and so on, but generators can live anywhere. Thor is also a very viable alternative to methadone or GLI, which are both very awesome and worthy starting points (and generators in their own right!). In fact, I owe a lot to Dave Copeland @davetron on Github for his super awesome ideas and directions on making cli applications in Ruby.
generating – what?
I have often started off a learning exercise by creating a gem skeleton
with bundler
:
gist 9439598 bundler-gem
This is itself a generator, and rather a cool way to start something up. However, it’s intended to create an actual gem, which I don’t necessarily want to do, especially if it’s something I’m trying to learn, or demonstrate, or somehow work out, but not something I’m going to publish in [rubygems][rubygems].
So what would I generate, exactly, and how?
A typical project directory
Below is a typical project directory structure I like for starting out:
gist 9439598 directory-structure
Several of these might remain empty or unused, but it’s a great
starting point for me. If I do wish to turn it into a gem, All I need
do is add a .gemspec
file.
gems
I also have my favourite set of gems that I like to start out with:
rspec
- my testing framework of choiceguard
- for continuous testingguard-rspec
- to watch rspec testsguard-bundler
- to watch changes to Gemfilepry
- my REPL of choicepry-nav
- nice navigation inside prypry-git
- link to git for blaming codepry-doc
- link to internal documentationpry-remote
- to attach to a remote ruby processpry-rescue
- to jump into the pry REPL if an exception happenspry-byebug
- debugger for ruby MRI 2.xpry-stack_explorer
- to jump up down or sideways in the execution stackawesome_print
- really nice looking output, integrated into pryrb-readline
- a necessary evil running on the Maccoolline
- nice colourisercoderay
- syntax highlighting on the fly in pry
These are in the Gemfile in a default group. If I were to release the
project as something, I’d move these into the :development
and
:test
groups.
continuous testing with guard
Guard is a fabulous system that
builds on top of pry that watches various
directories and reruns tests (or other commands if you set it up
to). I have the Guardfile in the generator, though, because
out-of-the-box guard-rspec
does not run rspec
bundled, and I’m
nearly always running under bundler
.
The modification is simple:
gist 9439598 Guardfile
You add the :cmd
to the rspec
watch
section. But it’s just one more thing to have
to remember and slow me down.
A difference between bundle gem
and guard-rspec
is that the former
expects spec tests to live directly under the spec/
directory, while
the latter figures the lib/
structure is mirrored under
spec/
. This isn’t such a big deal, but it is one more thing to think
about, and I’m often puzzled why things don’t work. (Convention over
configuration, perhaps not enough time spent together.)
so what about thor, again?
Thor seems all about generating things. Included in
Thor::Actions
are some really useful items for copying, converting, templating, and
so on. There are also commands for interacting with the user, should
that prove necessary.
Here’s the new project generator:
gist 9439598 new_ruby_project.thor
Let’s talk a bit about this script.
Thor::Group
The first thing we make is the class declaration for our thor
command. The class inherits from Thor::Group
instead of from Thor
proper. This causes the defined methods to be executed in order,
rather than creating individual sub-commands, as is the case when you
inherit from Thor
. This essentially turns the command into a script,
which is how we usually want things done in a generator.
Thor::Actions
We include Thor::Actions
in our class to give us the useful tools
for interacting with the user (say
, ask
, etc ), as well as the
building tools, create_file
, copy_file
, template
, and so on.
In this generator, I’m using template
nearly everywhere, as it
copies a source file with ERB
directives in it and writes out the
result to the destination.
argument :name
This is telling thor to expect one argument upon invocation, in this case, the name of the new ruby project.
def self.source_root
Thor::Action arguments typically follow the convention of source, destination, and options, with passing a block if there are further things that need to be done.
The source is determined by the class method source_root
. The
default method ends up using the current working directory as the
source root. Redefining the class method permits the author to specify
a completely different directory to use as the source root folder.
In this case, I’ve specified the skeleton directory for new projects, which looks like that above.
def name_components
This is doing a bit of munging on potential input from the user. It’s quite possible to put pretty much anything as the first argument, but what we really only want are the alpha-numeric bits, which we will consider as project name components.
Supplying things like Able & Louis: Go @@CRAXY@@
would end up as
name components ["Able", "Louis", "Go", "CRAXY"]
. The components are
used by later methods to construct useful names for things.
def snake_name
Here’s one now: out of something like ["Able", "Louis", "Go", "CRAXY"]
,
would come able_louis_go_craxy
, which is a very nice name for files
and directories, where this is usually used.
def camel_name
The other method using name_components
, this will produce
AbleLouisGoCraxy
which gives us our useful module name.
def copy_files
This picks up the files that we want to transfer to the root directory of the new project, translates them, and writes them to the destination’s root.
def dot_files
These are the “hidden” files in a directory, that begin with a “.” but are so useful. These are picked up from the source, translated, and saved to the destination with a “.” pre-pended.
def other_file
The rest of the project is translated and saved into the appropriate places.
what else?
This is still pretty blunt and could use some work to make it even more useful. It works for me now quite well. Here are some additional ideas:
use a manifest
Instead of hard-coding the source file names in the methods, create a manifest that lists what files should be moved from which locations to which destinations. I think a YAML file would do this quite nicely.
allow a different skeleton
Instead of nailing the skeleton inside the thor script, pass it in as a parameter.
I am sure I’ve reimplemented the wheel…
…but I am learning from doing this. There have been many ways to make gems, command line applications, web applications, and so on. I can see using this to build jekyll pages, or an entry for a new art project I’m working on to collect images, notes, etc.
Feedback
While I don’t use any sort of comment system here on the blog, feel free to leave comments at my Facebook Page or hit me up on Twitter. I’d love to hear from you.