Creating generators and executables with Thor By Nando Vieira July 13, 2015 Read in 7 minutes

Thor is an amazing library for creating generators. It gives you methods for creating and copying files and directories, defining symbolic links, read remote files, and more. And is the perfect companion for gems that need to generate a project structure, just like Rails.

The first thing you need is defining the generator class.

require 'thor' require 'thor/group' class MyGem::Generator < Thor :: Group include Thor :: Actions desc 'Generate a new filesystem structure' end

The Thor::Group class is perfect for generators because allows you to execute all actions at once. To define the actions of your generator, you must include the Thor::Actions module.

require 'thor' require 'thor/group' class MyGem::Generator < Thor :: Group include Thor :: Actions desc 'Generate a new filesystem structure' def self . source_root File . dirname ( __FILE__ ) + '/../../templates' end def create_config_file copy_file 'config.yml' , 'config/mygem.yml' end def create_git_files copy_file 'gitignore' , '.gitignore' create_file 'images/.gitkeep' create_file 'text/.gitkeep' end def create_output_directory empty_directory 'output' end end

As you can see, we’re defining the MyGem::Generator.source_root , which is your generator templates directory.

To execute this generator, you have to instantiate the MyGem::Generator class.

generator = MyGem :: Generator . new generator . destination_root = '/some/path' generator . invoke_all

We’re defining the destination through the generator.destination_root property; it’s optional and will use the current directory by default.

Creating an executable

You can create your CLI using the OptionParser standard library, although it can be really hard to manage the complexity for more sofisticated interfaces. But fear not; Thor can help you with that too.

To define a new CLI with Thor, create a class that inherits from the Thor class.

require 'thor' class MyGem::Cli < Thor end

Your executable file needs to call the MyGem::Cli.start method. You can pass an array that represents the ARGV , specially good when writing tests.

#!/usr/bin/env ruby require 'mygem' MyGem :: Cli . start

Your executable file must have execution permission; you can do it by running the chmod .

chmod +x bin/mygem

Now you can execute ./bin/mygem .

$ ./bin/mygem Commands: mygem help [COMMAND] # Describe available commands or one specific command

Our script doesn’t do much yet; let’s add an option for displaying the program version. To define a new command switch, you can use the Thor.map method.

module MyGem VERSION = '0.1.0' class Cli < Thor desc 'version' , 'Display version' map %w[-v --version] => :version def version say "MyGem #{ VERSION } " end end end

If you run ./bin/mygem again you’ll see that we now have a version command available. This happens because all public methods are considered as commands; define a private method if you don’t want to make it available as a command.

$ ./bin/mygem Commands: mygem help [COMMAND] # Describe available commands or one specific command mygem version # Display MyGem version

Now you use one of the following options to display the program version.

$ ./bin/mygem -v MyGem 0.1.0 $ ./bin/mygem --version MyGem 0.1.0 $ ./bin/mygem version MyGem 0.1.0

To create a new generator, you’ll probably want something like mygem new <some path> . This means that you need a positional argument, so just define a method that receives an argument.

require 'thor' module MyGem VERSION = '0.1.0' class Cli < Thor desc 'version' , 'Display MyGem version' map %w[-v --version] => :version def version say "MyGem #{ VERSION } " end desc 'new PATH' , 'Create a new static website' def new ( path ) path = File . expand_path ( path ) say "Creating site at #{ path } " end end end

Now you can execute the new command. Notice that Thor validates the argument presence for you, returning an error message when the argument is missing.

$ ./bin/mygem new foo Creating site at /Users/fnando/Projects/samples/thor-bin-sample/foo $ ./bin/mygem new ERROR: "mygem new" was called with no arguments Usage: "mygem new PATH"

You may need to accept some options for initializing your project. Rails does this all the time, like rails new myapp -d postgresql . To define a new option for your command, use the Thor.option method.

require 'thor' module MyGem VERSION = '0.1.0' class Cli < Thor desc 'version' , 'Display MyGem version' map %w[-v --version] => :version def version say "MyGem #{ VERSION } " end desc 'new PATH' , 'Create a new static website' option :javascript_engine , :default => 'babeljs' , :aliases => '-j' def new ( path ) path = File . expand_path ( path ) say "Creating site at #{ path } " say options end end end

In this example, we can define the JavaScript engine by using --javascript-engine or its -j alias. Thor is smart and will automatically convert the option name to a hyphenated version.

$ ./bin/mygem new foo Creating site at /Users/fnando/Projects/samples/thor-bin-sample/foo {"javascript_engine"=>"babeljs"} $ ./bin/mygem new foo --javascript-engine coffeescript Creating site at /Users/fnando/Projects/samples/thor-bin-sample/foo {"javascript_engine"=>"coffeescript"} $ ./bin/mygem new foo -j coffeescript Creating site at /Users/fnando/Projects/samples/thor-bin-sample/foo {"javascript_engine"=>"coffeescript"}

The Thor.option accepts other parameters, like type coercion and if it’s a required option.

option :file , :type => :array , :aliases => :files option :force , :type => :boolean , :default => false option :database , :required => true

By default Thor won’t validate the options you’re passing to the executable. So when you combine a positional argument with an invalid option, you’ll end up with a unexpected behavior.

$ ./bin/mygem new -h Creating site at /Users/fnando/Projects/samples/thor-bin-sample/-h {"javascript_engine"=>"babeljs"}

Did you see that the directory name is marked as -h ? Thor thinks that -h is the positional argument, instead of an invalid option. You can reject invalid options by calling the Thor.check_unknown_options! method.

require 'thor' module MyGem VERSION = '0.1.0' class Cli < Thor check_unknown_options! desc 'version' , 'Display MyGem version' map %w[-v --version] => :version def version say "MyGem #{ VERSION } " end desc 'new PATH' , 'Create a new static website' option :javascript_engine , :default => 'babeljs' , :aliases => '-j' def new ( path ) path = File . expand_path ( path ) say "Creating site at #{ path } " say options end end end

Now, invalid arguments are rejected and an error message is displayed.

$ ./bin/mygem new -h Unknown switches '-h'

Another common task is having global options like --verbose , which can be applied to all commands. You can use the Thor.class_option for this.

require 'thor' module MyGem VERSION = '0.1.0' class Cli < Thor check_unknown_options! desc 'version' , 'Display MyGem version' map %w[-v --version] => :version class_option 'verbose' , :type => :boolean , :default => false def version say "MyGem #{ VERSION } " end desc 'new PATH' , 'Create a new static website' option :javascript_engine , :default => 'babeljs' , :aliases => '-j' def new ( path ) path = File . expand_path ( path ) say "Creating site at #{ path } " say options end end end

With this option you can set the --verbose in every command and the Thor#options method will be populated with global and local options.

$ ./bin/mygem new foo --verbose Creating site at /Users/fnando/Projects/samples/thor-bin-sample/foo {"verbose"=>true, "javascript_engine"=>"babeljs"}

Eventually you’ll have to validate the arguments you’re receiving. You may even need to interrupt the execution, returning an error message. You can thrown exception with Thor::Error class, specifying the error message you want to display.

require 'thor' module MyGem VERSION = '0.1.0' class Cli < Thor check_unknown_options! desc 'version' , 'Display MyGem version' map %w[-v --version] => :version class_option 'verbose' , :type => :boolean , :default => false def version say "MyGem #{ VERSION } " end desc 'new PATH' , 'Create a new static website' option :javascript_engine , :default => 'babeljs' , :aliases => '-j' def new ( path ) path = File . expand_path ( path ) raise Error , "ERROR: #{ path } already exists." if File . exist? ( path ) say "Creating site at #{ path } " say options end end end

We’re returning an error message when the output directory already exists. The problem, in this case, is that the exit code of that failing command will be 0 , which means “success” in the *nix world.

$ ./bin/mygem new cli.rb --verbose ERROR: /Users/fnando/Projects/samples/thor-bin-sample/cli.rb already exists. $ echo $? 0

You can fix this by defining the MyGem::Cli.exit_on_failure? method and returning true .

require 'thor' module MyGem VERSION = '0.1.0' class Cli < Thor check_unknown_options! def self . exit_on_failure? true end desc 'version' , 'Display MyGem version' map %w[-v --version] => :version class_option 'verbose' , :type => :boolean , :default => false def version say "MyGem #{ VERSION } " end desc 'new PATH' , 'Create a new static website' option :javascript_engine , :default => 'babeljs' , :aliases => '-j' def new ( path ) path = File . expand_path ( path ) raise Error , "ERROR: #{ path } already exists." if File . exist? ( path ) say "Creating site at #{ path } " say options end end end

Now the same execution will return the exit code as 1 .

$ ./bin/mygem new cli.rb --verbose ERROR: /Users/fnando/Projects/samples/thor-bin-sample/cli.rb already exists. $ echo $? 1

You can even use colored output for better visualization; just use the set_color method.

require 'thor' module MyGem VERSION = '0.1.0' class Cli < Thor check_unknown_options! def self . exit_on_failure? true end desc 'version' , 'Display MyGem version' map %w[-v --version] => :version class_option 'verbose' , :type => :boolean , :default => false def version say "MyGem #{ VERSION } " end desc 'new PATH' , 'Create a new static website' option :javascript_engine , :default => 'babeljs' , :aliases => '-j' def new ( path ) path = File . expand_path ( path ) raise Error , set_color ( "ERROR: #{ path } already exists." , :red ) if File . exist? ( path ) say "Creating site at #{ path } " say options end end end

Now the same error message would be displayed like this:

Wrapping up

I really like Thor for creating CLIs and generators. It has all the features I need and is used by large projects such as Ruby on Rails.

To get more information about Thor, check out the documentation. The project’s wiki also have some useful information.