Ruby Thor CLI: Building Professional Command-Line Tools
If you’ve ever used Bundler, Rails, or Rake, you’ve interacted with Thor—Ruby’s most popular CLI building library. Thor powers some of the most widely-used Ruby tools, and for good reason: it makes building command-line interfaces almost effortless.
In this guide, you’ll learn how to create professional CLI tools with Thor. We’ll build a real example—a deployment CLI—that demonstrates commands, options, subcommands, and interactive prompts. By the end, you’ll have the skills to build CLI tools that feel as polished as the ones you use daily.
Before you begin
Thor works best when you already know the command you want to expose and you want a clean way to present it to the user. It sits between plain Ruby code and the finished command-line interface, which makes it a good fit for tools that need structure without a lot of boilerplate. That is why so many Ruby projects use it for generators, deploy scripts, and project utilities.
The real value of Thor is not just less typing. It is the way the library turns your Ruby methods into a discoverable interface. Help text, options, subcommands, and argument parsing all come from the same place, so the code stays close to the command it implements. That makes it easier to maintain when the CLI grows from a single command into a small toolset.
Why Thor?
Before diving in, let’s understand why Thor is the go-to choice for Ruby CLI development.
Thor provides three key benefits that would otherwise require significant boilerplate:
- Automatic argument parsing — Convert CLI arguments into method parameters
- Built-in help generation —
--helpworks automatically - Clean DSL — Define commands using simple Ruby method definitions
Compare this to parsing options manually or using Ruby’s built-in OptionParser. Thor reduces hundreds of lines of code to a few declarative statements.
That difference matters once a CLI has more than one or two commands. The help text, argument parsing, and command dispatching stay in one place, which keeps the tool easier to extend and easier to teach to teammates.
Installation
Thor is distributed as a gem and requires no external dependencies:
gem install thor
Or add it to your Gemfile if you’re building a gem:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
# In your Gemfile
gem 'thor', '~> 1.3'
your first Thor CLI
Let’s start with the simplest possible CLI. Create a file called hello.rb with a Thor class that exposes a single hello command. The desc method documents the command for the auto-generated help output:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
require 'thor'
class HelloCLI < Thor
desc "hello NAME", "Greet a person by name"
def hello(name)
puts "Hello, #{name}!"
end
end
HelloCLI.start(ARGV)
Run the script by passing hello and a name as command-line arguments. Thor uses the first argument as the command name and the rest as method arguments:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
ruby hello.rb hello World
# => Hello, World!
ruby hello.rb hello --help
# => Usage:
# hello.rb hello NAME
#
# Greet a person by name
That’s it: you have a working CLI with argument parsing and automatic help. The desc method documents your command, and Thor uses that documentation to generate help text.
The nice part is that the command body stays ordinary Ruby. You can validate input, read configuration, and call services without leaving the language you already know.
building a real deployment CLI
Now let’s build something more practical: a deployment CLI with multiple commands, options, and subcommands.
Project Structure
A well-organized CLI typically looks like this:
my_cli/
├── bin/
│ └── deploy # Executable entry point
├── lib/
│ ├── my_cli/
│ │ └── commands.rb # Thor classes
│ └── my_cli.rb # Main require
└── Gemfile
the executable
Create bin/deploy:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
#!/usr/bin/env ruby
require_relative '../lib/my_cli'
MyCLI::Commands.start(ARGV)
Make the file executable so it can be run directly from the terminal. Without this step, you would need to prefix every command with ruby:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
chmod +x bin/deploy
defining commands
Create lib/my_cli/commands.rb:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
require 'thor'
module MyCLI
class Commands < Thor
desc "deploy ENVIRONMENT", "Deploy the application to an environment"
option :force, type: :boolean, default: false,
desc: "Skip confirmation prompts"
option :tag, type: :string,
desc: "Git tag to deploy"
long_desc <<-LONGDESC
Deploys the application to the specified environment (staging, production).
Examples:
$ deploy staging
$ deploy production --tag v1.2.3
$ deploy production --force
LONGDESC
def deploy(environment)
validate_environment!(environment)
tag = options[:tag] || fetch_latest_tag
puts "Deploying #{tag} to #{environment}..."
unless options[:force]
confirm_deploy(environment, tag)
end
run_deployment(environment, tag)
puts "Deployment complete!"
end
desc "rollback [ENVIRONMENT]", "Rollback to the previous release"
option :steps, type: :numeric, default: 1,
desc: "Number of releases to rollback"
def rollback(environment = "production")
puts "Rolling back #{options[:steps]} release(s) in #{environment}..."
# Rollback logic here
end
desc "status", "Show deployment status"
def status
puts "Production: v1.2.3 (deployed 2 hours ago)"
puts "Staging: v1.2.3-rc1 (deployed 5 minutes ago)"
end
private
def validate_environment!(env)
unless %w[staging production].include?(env)
puts "Error: Invalid environment. Use 'staging' or 'production'."
exit 1
end
end
def fetch_latest_tag
# In reality, fetch from git
`git describe --tags --abbrev=0`.strip
end
def confirm_deploy(env, tag)
print "Deploy #{tag} to #{env}? (y/n) "
answer = STDIN.gets.chomp
exit unless answer.downcase == 'y'
end
def run_deployment(env, tag)
# Actual deployment logic
sleep 1 # Simulate deployment time
end
end
end
This example demonstrates several Thor features:
- Required arguments —
environmentindeployis positional and required - Options with types —
--forceis a boolean,--tagis a string - Default values —
--forcedefaults tofalse - Long descriptions — Provide detailed documentation accessible via
--help - Private methods — Methods starting with
_aren’t exposed as commands - Exit codes — Use
exit 1for errors
When the CLI grows, those features help keep the user experience predictable. A well-named command, clear options, and consistent exit codes matter just as much as the Ruby code behind them.
running the CLI
# Deploy to staging
./bin/deploy staging
# Deploy to production with a specific tag
./bin/deploy production --tag v1.2.3
# Force deploy without confirmation
./bin/deploy production --tag v1.2.3 --force
# View help
./bin/deploy help
./bin/deploy help deploy
Subcommands
As your CLI grows, you may want to organize commands into groups. Thor supports this through subcommands. Let’s add a logs command group:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
require 'thor'
module MyCLI
class Commands < Thor
desc "deploy ENVIRONMENT", "Deploy the application"
def deploy(environment)
# ...
end
desc "rollback [ENVIRONMENT]", "Rollback deployments"
def rollback(environment = "production")
# ...
end
end
class Logs < Thor
desc "logs ENVIRONMENT", "View application logs"
option :lines, type: :numeric, default: 100,
desc: "Number of lines to show"
option :follow, type: :boolean, default: false,
desc: "Stream logs in real-time"
def logs(environment)
puts "Showing #{options[:lines]} log lines from #{environment}"
# Log streaming logic
end
desc "search PATTERN", "Search logs for a pattern"
def search(pattern)
puts "Searching logs for: #{pattern}"
end
end
end
Register subcommands in your main CLI:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
require 'thor'
module MyCLI
class Commands < Thor
desc "deploy ENVIRONMENT", "Deploy the application"
def deploy(environment)
# ...
end
# Register the Logs subcommand
subcommand "logs", Logs
end
end
Now your CLI supports:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
./bin/deploy logs production --lines 500
./bin/deploy logs production --follow
./bin/deploy logs search "ERROR"
interactive prompts
For sensitive operations, you might want interactive confirmation. Thor works well with the highline gem for this, which provides methods like agree and ask for getting user input:
require 'thor'
require 'highline/import'
class DeployCLI < Thor
desc "destroy ENVIRONMENT", "Permanently destroy environment data"
def destroy(environment)
say "Warning: This will permanently delete all data in #{environment}!", :red
unless agree("Are you sure? ") { |q| q.default = 'n' }
say "Aborted.", :yellow
exit 0
end
type = agree("Type '#{environment}' to confirm: ") { |q| q.validate = /#{environment}/ }
# Proceed with destruction
end
end
global options
Sometimes you want options available across all commands. Use class_options at the class level instead of repeating option inside each method. These options are inherited by every command in the class:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
class GlobalCLI < Thor
class_option :verbose, type: :boolean, default: false,
desc: "Enable verbose output"
class_option :config, type: :string,
desc: "Path to config file"
desc "deploy ENVIRONMENT", "Deploy application"
def deploy(environment)
if options[:verbose]
puts "Verbose mode enabled"
puts "Config: #{options[:config]}"
end
# ...
end
desc "rollback ENVIRONMENT", "Rollback deployment"
def rollback(environment)
# Options are available here too
end
end
Now --verbose and --config work with any command in the class. The options are available through options[:verbose] and options[:config] inside every method:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
./deploy deploy production --verbose
./deploy rollback staging --config ~/.deploy.yml
Best Practices
Follow these patterns to build CLI tools that users love:
1. Always provide help
Use desc and long_desc to document every command. Thor generates --help automatically:
The first code block establishes a foundational Ruby concept, and the second builds on it by introducing a related technique or showing a different angle. Reading these examples in sequence helps you build a layered understanding of how Ruby features work together in practice. The progression from basic to more advanced usage mirrors how you would naturally encounter these patterns when reading or writing real Ruby code.
desc "deploy ENVIRONMENT", "Deploy to an environment"
long_desc <<-LONGDESC
Deploys your application using the configured deployment strategy.
Supported environments:
- staging: Testing environment
- production: Live environment
LONGDESC
2. Use exit codes correctly
Return 0 for success, non-zero for errors:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
def deploy(environment)
begin
# Deployment logic
rescue => e
puts "Error: #{e.message}"
exit 1
end
end
3. Handle missing arguments
Thor automatically validates required arguments, but you can customize behavior:
Methods and classes are the building blocks of Ruby programs. The first example introduces a pattern, and the second shows a variation that handles a different use case. Learning to recognise these variations helps you read Ruby code more fluently and choose the right pattern for the problem you are solving. Each variation trades off simplicity, flexibility, and explicitness.
def deploy(environment = nil)
if environment.nil?
puts "Error: ENVIRONMENT is required"
puts "Usage: deploy ENVIRONMENT"
exit 1
end
# ...
end
4. Test Your CLI
Use Thor’s built-in testing support:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
require 'thor/spec'
describe MyCLI::Commands do
include Thor::Spec::Fixtures
it "deploys to the specified environment" do
capture_io { subject.deploy("staging") }.should include("Deploying")
end
end
FAQ
How do I handle environment variables in Thor?
Access them directly in your commands:
Thor commands gain their power from how options and arguments compose together. The first code block shows one piece of the CLI interface, and the second demonstrates how additional features like type checking, default values, and help text enrich the user experience. Building a polished CLI means thinking about each flag and argument as part of a discoverable interface that users can explore with —help.
def deploy(environment)
api_key = ENV['DEPLOY_API_KEY']
unless api_key
puts "Error: Set DEPLOY_API_KEY environment variable"
exit 1
end
end
can i use thor with rails?
Yes! Many Rails gems use Thor for generators and rake tasks. Add it to your Gemfile and use it just like in any Ruby project.
how do I add a version command?
Map the --version and -v flags to a version method using the map method. This lets users check the CLI version without typing a separate command:
class CLI < Thor
map ["--version", "-v"] => :version
desc "version", "Show version"
def version
puts "MyCLI v1.2.3"
end
end
What’s the difference between Thor and Rake?
Thor is designed for CLI applications—you define commands that accept arguments. Rake is for running tasks, typically within a project. Rails uses Thor for its CLI (generators, migrations) and Rake for project tasks.
Next steps
If you want to turn command-line tasks into scheduled automation, continue with Ruby Rake Tasks. Rake is a good companion to Thor when you need project-level build and maintenance tasks alongside a user-facing CLI.
If you want to compare Thor with a different automation style, move next to Automating Tasks with Rake. Rake is a better fit when you want project tasks instead of interactive commands, while Thor shines when users need a polished CLI with options and subcommands.
Conclusion
Thor transforms CLI development from a chore into a pleasure. With just a few lines of Ruby, you get argument parsing, help generation, subcommands, and a clean DSL—all the ingredients for professional command-line tools.
The deployment CLI we built demonstrates patterns you can apply to any project: required and optional arguments, boolean and string options, subcommand groups, and proper exit codes. Start with these foundations, and you’ll build CLI tools that feel as polished as Bundler or Rails.
Next Steps
Ready to continue your Ruby for DevOps journey? The next tutorial in this series covers Automating Tasks with Rake—Ruby’s built-in task management tool. You’ll learn how to create reusable automation tasks, build task dependencies, and organize your DevOps workflows.
After mastering Rake, explore other tutorials in the series:
- Ruby Gem Development — Package your CLI as a distributable gem
- Ruby Testing — Write comprehensive tests for your tools