homeASCIIcasts

242: Thor 

(view original Railscast)

Other translations: Ru Es

All Rails developers will be familiar to some degree with Rake. Rake was originally written as an alternative to the make command but in Rails it’s mostly used for small administrative scripts. As a scripting solution Rake can be a little limiting, for example passing arguments to a Rake task is not pretty and usually we have to resort to passing them as environment variables. Another problem with Rake is that tasks cannot easily be made global, they are always local to the current project. Tools such as Sake get around this problem but then you require that additional tool.

In this episode we’ll take a look a Thor. Thor is an alternative to Rake which doesn’t have the limitations mentioned above. It is included as a dependency in Rails so if you have Rails installed then you automatically have Thor as Rails’ generators make use of it. Therefore learning Thor will help if you need to create any generators.

Thor can be run from the command line and if we run thor help we’ll get a list of the options that it supports.

$ thor help
Tasks:
  thor help [TASK]     # Describe available tasks or one specific task
  thor install NAME    # Install an optionally named Thor file into your system tasks
  thor installed       # List the installed Thor modules and tasks
  thor list [SEARCH]   # List the available thor tasks (--substring means .*SEARCH)
  thor uninstall NAME  # Uninstall a named Thor module
  thor update NAME     # Update a Thor file from its original location
  thor version         # Show Thor version

We don’t have any Thor scripts of our own yet. Let’s create one now.

A Script to Copy Configuration Files

If we’re going to create a Thor script we might as well create one that does something useful. It is normal in a Rails application to not put certain configuration files under source control as they contain sensitive information such as passwords. We’re going to create some example configuration files in an examples subdirectory then create a Thor script that will copy these files over into the config directory.

Thor scripts can be put in an application’s lib/tasks directory, like Rake tasks, so we’ll create a new file in this directory called setup.thor. A Thor script is a class that inherits from Thor and the name of the class will become the namespace for the commands. Each method in the class becomes a command so long as it has a description. This is defined using the desc method, which takes two arguments: the name of the command and a description. We’ll start by creating a simple command called config that will output a line of text.

/lib/tasks/setup.thor

class Setup < Thor
  desc "config", "copy configuration files"
  def config
    puts "running config"
  end
end

We can run the command by running thor setup:config. This calls the config method and we’ll see the output printed in the terminal window.

$ thor setup:config
running config

To see the list of available commands we can run thor list.

$ thor list
setup
-----
thor setup:config  # copy configuration files

In the output is the command we’ve just written along with its description.

Copying Files

Let’s make the config command do something useful. The code below loops through each file in the /config/examples directory and copies it to /config, skipping the file if it already exists there.

/lib/tasks/setup.thor

class Setup < Thor
  desc "config", "copy configuration files"
  def config
    Dir["config/examples/*"].each do |source|
      destination = "config/#{File.basename(source)}"
      if File.exist?(destination)
        puts "Skipping #{destination} because it already exists"
      else
        puts "Generating #{destination}"
        FileUtils.cp(source, destination)
      end
    end
  end
end

If we run the config command again now it should copy the files over.

$ thor setup:config
Generating config/database.yml
Generating config/private.yml

When we run the command again it will skip the files as they already exist in the config directory.

$ thor setup:config
Skipping config/database.yml because it already exists
Skipping config/private.yml because it already exists

It would be useful if we could pass in a --force option so that if the files exist in the destination directory they are still replaced. We can do this by calling method_options before the method definitions to define the options.

/lib/tasks/setup.thor

class Setup < Thor
  desc "config", "copy configuration files"
  method_options :force => :boolean
  def config
    Dir["config/examples/*"].each do |source|
      destination = "config/#{File.basename(source)}"
      FileUtils.rm(destination) if options[:force]
      if File.exist?(destination)
        puts "Skipping #{destination} because it already exists"
      else
        puts "Generating #{destination}"
        FileUtils.cp(source, destination)
      end
    end
  end
end

We can add as many options as we want here and there are a number of different types that are supported: strings, numbers and so on. To get the value for a given option we call options and in the code above we use options[:force] to read the :force option and delete the file if force has been set.

If we run the file with the --force option now the existing files are overwritten.

$ thor setup:config --force
Generating config/database.yml
Generating config/private.yml

Adding Options

Any additional arguments that we pass to thor are passed to the method. Let’s say that want a way to customize the files that are copied over so that if we just want private.yml to be copied we could run

$ thor setup:config private.yml

This argument will be passed to the config method. We don’t want to be forced to specify a file name so we’ll give the argument a default value of "*" so that all of the files are copied. It’s good to keep the documentation accurate so we’ll also update the description so that it mentions the NAME argument.

/lib/tasks/setup.thor

class Setup < Thor
  desc "config [NAME]", "copy configuration files"
  method_options :force => :boolean
  def config(name = "*")
    Dir["config/examples/#{name}"].each do |source|
      destination = "config/#{File.basename(source)}"
      FileUtils.rm(destination) if options[:force]
      if File.exist?(destination)
        puts "Skipping #{destination} because it already exists"
      else
        puts "Generating #{destination}"
        FileUtils.cp(source, destination)
      end
    end
  end
end

We’ll call the command with an argument now to see if it works.

$ thor setup:config private.yml
Skipping config/private.yml because it already exists

It does, and also works if we can also specify a name in conjunction with --force.

$ thor setup:config  private.yml --force
Generating config/private.yml

Installing Commands Globally

Our script is now pretty useful and we’d like to use it other Rails applications. Thor makes this easy: all we have to do is call thor install <path_to_file> and this will install the command into the system Thor commands.

$ thor install lib/tasks/setup.thor
Your Thorfile contains:
class Setup < Thor
  desc "config [NAME]", "copy configuration files"
  method_options :force => :boolean
  def config(name = "*")
    Dir["config/examples/#{name}"].each do |source|
      destination = "config/#{File.basename(source)}"
      FileUtils.rm(destination) if options[:force]
      if File.exist?(destination)
        puts "Skipping #{destination} because it already exists"
      else
        puts "Generating #{destination}"
        FileUtils.cp(source, destination)
      end
    end
  end
end
Do you wish to continue [y/N]? y
Please specify a name for lib/tasks/setup.thor in the system repository [setup.thor]: 
Storing thor file in your system repository

Once it’s been installed we can run thor list from any directory and we’ll see the command listed.

$ cd ~
$ thor list
setup
-----
thor setup:config [NAME]  # copy configuration files

Our command is now globally available and we can use it in any of our Rails apps.

Accessing a Rails application from Thor

There are a couple of other things we’ll demonstrate in Thor and for these we’ll create a new command in our Setup class. This command will generate some records in our database so we’ll call it populate. Our application is a blogging app with an Article model and populate will create ten articles for us.

/lib/tasks/setup.thor

class Setup < Thor
  desc "config [NAME]", "copy configuration files"
  method_options :force => :boolean
  def config(name = "*")
	# Config method body omitted.
  end
  desc "populate", "generate records"
  def populate
    10.times do |num|
      puts "Generating article #{num}"
      Article.create!(:name => "Article #{num}")
    end
  end
end

If we try to run this command now we’ll get an error message saying that the Article class cannot be found and this is because the command hasn’t loaded the application’s models. The Rails application isn’t loaded by default in the context of a Thor command and so we need to load the application before the command tries to create an Article. Fortunately this is not difficult to do, all we need to do is require the config/environment file.

/lib/tasks/setup.thor

class Setup < Thor
  desc "config [NAME]", "copy configuration files"
  method_options :force => :boolean
  def config(name = "*")
	# Config method body omitted.
  end
  desc "populate", "generate records"
  def populate
    require ‘./config/environment’
    10.times do |num|
      puts "Generating article #{num}"
      Article.create!(:name => "Article #{num}")
    end
  end
end

If we run the command again now it should create the ten articles, after a short delay while the environment loads.

$ thor setup:populate
Generating article 0
Generating article 1
Generating article 2
Generating article 3
Generating article 4
Generating article 5
Generating article 6
Generating article 7
Generating article 8
Generating article 9

It would be good if we could make the number of articles created configurable so that we can create any number we want. We can do this by using method_options as we did in the config method.

/lib/tasks/setup.thor

class Setup < Thor
  desc "config [NAME]", "copy configuration files"
  method_options :force => :boolean
  def config(name = "*")
	# Config method body omitted.
  end
  desc "populate", "generate records"
  method_options :count => 10
  def populate
    require './config/environment'
    options[:count].times do |num|
      puts "Generating article #{num}"
      Article.create!(:name => "Article #{num}")
    end
  end
end

This time instead of specifying a type for the options we’ve specified a default value and Thor will infer the type from this value. Now if we pass in a count of 5, five articles will be created.

$ thor setup:populate --count 5
Generating article 0
Generating article 1
Generating article 2
Generating article 3
Generating article 4

That’s it for this episode on Thor. If you want more information about it the documentation is a great place to start, especially if you want to know more about passing in options.

The big question is when should you use Thor over Rake? If you’re creating a simple Rails application then it’s better to stick with Rake as that is the most popular and well-known. If you find yourself creating a lot of administration tasks for your Rails applications then Thor is definitely worth considering.