Thor is a CLI building tool for Ruby. It is a more robust solution than the built-in optparse module and is more appropriate for larger scripts with multiple commands or subcommands.

I intend for this post to primarily augment the document on the Thor website. There are several features of Thor that are not documented on the site that I find useful in day-to-day use.

Quick start

Following is a simple example of a CLI application built using Thor. It has one command hello that, greets the user.

# example taken from whatisthor.com

require "thor"

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  def hello(name)
    puts "Hello #{name}"
  end
end

MyCLI.start(ARGV)

Exit on failure

If you run the program in the [[#Quick start]] without providing a name, you will get the following output, and the exit status will be 0.

ERROR: "cli.rb hello" was called with no arguments
Usage: "cli.rb hello NAME"
Deprecation warning: Thor exit with status 0 on errors. To keep this behavior, you must define `exit_on_failure?` in `MyCLI`
You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.

To get rid of the warning, add an exit_on_failure? static method to your class.

class MyCLI < Thor
  def self.exit_on_failure?
    true
  end

  # ...
end

If the value returned is true, you will get an exit status of 1 on error. If it is false, you will get an exit status of 0. I will always set this to true because it will work as expected in shell scripts, and it does not have a meaningful impact when calling it directly from a terminal barring any personalization that deals with exit values.

Options

You can add options by specifying option metadata like the following:

# example taken from whatisthor.com

require "thor"

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  option :from
  def hello(name)
    puts "from: #{options[:from]}" if options[:from]
    puts "Hello #{name}"
  end
end

MyCLI.start(ARGV)

## Example usage
#
# > `ruby ./cli.rb hello --from Fred Wilma`
# from: Fred
# Hello Wilma

Configuring options

There are several parameters you can use to configure an option. The ones I use the most are required, default, type, alias, and desc. Here is an extension of the example above.

require "thor"

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  option :from, default: "your secret admirer", desc: "Who is doing the greeting", aliases: [:f]
  def hello(name)
    puts "from: #{options[:from]}"
    puts "Hello #{name}"
  end
end

Note that we drop the if from the line where we print the from:. Since the from option provides a default, it will always have a value. Since we provide an alias (:f), we can invoke the program like ruby ./cli.rb hello -f Fred Wilma and get the same result.

Options are strings by default, but can be one of several types.

Note that you can also defined an option at the class level, and it will apply to all methods in the class.

Enums

One particular configuration option I would like to point out is enum. I often write scripts that have an option where the value belongs to a list of acceptable values, such as environment for running software.

Following is an example of a CLI that requires an environment option to be one of local, testing, or production.

class MyCLI < Thor
  def self.exit_on_failure?
    true
  end

  desc "reset-db", "Reset the database"
  option :environment, desc: "The env for the db", default: "local", enum: %w[local testing production], aliases: :e
  def reset_db
    puts "Resetting the database in the #{options[:environment]} environment..."
    # ...
  end
end

## Example usage
#
# > ruby ./cli.rb reset-db
# Resetting the database in the local environment...
#
# > ruby ./cli.rb reset-db -e testing
# Resetting the database in the testing environment...
#
# > ruby ./cli.rb reset-db -e other
# Expected '--environment' to be one of local, testing, production; got other

Note that the help includes the values as well.

> ruby ./cli.rb help reset-db
Usage:
  cli.rb reset-db

Options:
  -e, [--environment=ENVIRONMENT]  # The env for the db
                                   # Default: local
                                   # Possible values: local, testing, production

Reset the database

Setting the default command

Note that most of the examples in this post are applications with only one command. It is possible by specifying the default_command on the class.

class MyCLI < Thor
  default_command :hello

  desc "hello", "say hello"
  option :name, required: true
  def hello
    puts "Hello #{options[:name]}"
  end
end

## Example usage
#
# > ruby ./cli.rb --name Wilma
# Hello Wilma

The catch is that this does not work well if the default command requires arguments. Consider the following example.

class MyCLI < Thor
  default_command :hello

  def self.exit_on_failure?
    true
  end

  desc "hello NAME", "say hello to NAME"
  option :from
  def hello(name)
    puts "from: #{options[:from]}" if options[:from]
    puts "Hello #{name}"
  end
end

Note that NAME is a required argument.

If we run this without specifying the command and providing the name, we get the following.

> ruby ./cli.rb Wilma
Could not find command "Wilma".

However, if we specify the :from option, we get a different result.

> ruby ./cli.rb --from Fred Wilma
from: Fred
Hello Wilma

This makes sense because Thor can tell based on the option that we are not specifying a command. However, when we do not have an option, it cannot tell the difference between a command and an argument.

I think a reasonable rule of thumb is that if you set a default command, it should not have arguments. Any configuration should be presented as options, some of which may be required.