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
.