This is a guest blog posted on rubylearning.com
Introduction
Ruby, as a dynamic language, is always used for quick processing command-line tool for its simplicity and productivity.
This article talks about three ways to write a command-line tool.
Before we start, there are a few things you need to know:
- Put line
#!/usr/bin/env ruby into the first line of your command-line file which will tell shell execute your file use Ruby
- Make sure your file is executable, run
chmod u+x FILE_PATH
- Print help text if user use it in wrong way
Other people will not sure how to execute your command-line tool.
Conventions
I’ll use three definitions:
- Command-line file name
- Command
- Option
For example there is a command: ‘server start -e development’
- Command-line file name is ‘server’
- Command is the first argument ‘start’
- Option is the reset of argument pair ‘-e development’
Let’s go
We start from a simple example: write a command-line tool to start and stop the server.
Without any lib
case ARGV[0]
when "start"
STDOUT.puts "called start"
when "start"
STDOUT.puts "called stop"
else
STDOUT.puts <<-EOF
Please provide command name
Usage:
server start
server stop
EOF
end
ARGV, all arguments will stored as a array in this variable.
What if you need to pass some option?
def parse_options
options = {}
case ARGV[1]
when "-e"
options[:e] = ARGV[2]
when "-d"
options[:d] = ARGV[2]
end
options
end
case ARGV[0]
when "start"
STDOUT.puts "start on #{parse_options.inspect}"
when "stop"
STDOUT.puts "stop on #{parse_options.inspect}"
else
STDOUT.puts <<-EOF
Please provide command name
Usage:
server start
server stop
options:
-e ENVIRONMENT. Default: development
-d daemon, true or false. Default: true
EOF
end
This code is simple but it has some disadvantages:
- Writing option parser and help text in different places will bring you troubles when they are not matched.
- Using array index to get options from ARGV, these magic numbers will create maintenance problem.
OptionParser
OptionParser is build-in ruby lib help you parse arguments.
we can refactor our code like this:
require 'optparse'
options = {}
opt_parser = OptionParser.new do |opt|
opt.banner = "Usage: opt_parser COMMAND [OPTIONS]"
opt.separator ""
opt.separator "Commands"
opt.separator " start: start server"
opt.separator " stop: stop server"
opt.separator ""
opt.separator "Options"
opt.on("-e","--environment ENVIRONMENT",
"Which environment you want server run. Default: development") do |environment|
options[:environment] = environment
end
opt.on("-d","--daemon","running on daemon mode? Default: true") do
options[:daemon] = true
end
opt.on("-h","--help","help") do
puts opt_parser
end
end
opt_parser.parse!
case ARGV[0]
when "start"
puts "call start on options #{options.inspect}"
when "stop"
puts "call stop on options #{options.inspect}"
else
puts opt_parser
end
Try to execute this file without argument, you’ll find it prints very nice help text.
opt_parser.parse! is the method extract options from ARGV, extracted value will be deleted from ARGV.
OptionParser is better than that.
You can define options value type, then OptionParser will convert value to the type you defined like this:
opt.on("-e","--environment ENVIRONMENT",Numeric,
"which environment you want server run") do |environment|
options[:environment] = environment
end
opt.on("--delay N", Float, "Delay N seconds before executing") do |n|
options[:delay] = n
end
opt.on("-j x,y,z","--jurisdictions x,y,z", Array,
"which jurisdiction will start") do |jurisdictions|
options[:jurisdictions] = jurisdictions
end
server_list = %w[a b c]
opt.on("-s SERVERS","--servers SERVERS", server_list,
"which server will start between #{server_list.join(',')}") do |servers|
options[:servers] = servers
end
You can mark whether value of the option is mandatory.
# Mandatory argument.
opts.on("-r", "--require LIBRARY",
"Require the LIBRARY before executing your script") do |lib|
options.library << lib
end
# Optional argument; multi-line description.
opts.on("-i", "--inplace [EXTENSION]",
"Edit ARGV files in place",
" (make backup if EXTENSION supplied)") do |ext|
options.inplace = true
options.extension = ext || ''
options.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot.
end
For more details your can see this article and ruby rdoc
Benefit of OptionParser is: we don’t need to use array index to retrieve options and we write help text along with option definition.
Disadvantage of OptionParser is: since different commands need using the same option parser, you cannot define different option parsers for different commands. To solve this problem, you can resort to Thor.
Thor
As you know Thor is a replacement of Rake. Let’s see how we use Thor to refactor our command-line tool.
require 'rubygems'
require 'thor'
class ThorExample < Thor
desc "start", "start server"
method_option :environment,:default => "development", :aliases => "-e",
:desc => "which environment you want server run."
method_option :daemon, :type => :boolean, :default => false, :aliases => "-d",
:desc => "running on daemon mode?"
def start
puts "start #{options.inspect}"
end
desc "stop" ,"stop server"
method_option :delay, :default => 0, :aliases => "-w",
:desc => "wait server finish it's job"
def stop
puts "stop"
end
end
ThorExample.start
desc defines command name and long description
method_option define option parser for this command
ThorExample.start is method to start parse argument
Execute it without argument, the output is:
Tasks:
thor_example help [TASK] # Describe available tasks or one specific task
thor_example start # start server
thor_example stop # stop server
Execute it with argument help start, you’ll get help text for command start:
Usage:
thor_example start
Options:
-e, [--environment=ENVIRONMENT] # which environment you want server run.
# Default: development
-d, [--daemon] # running on daemon mode?
start server
As you can see, it’s very clean and easy to write.
For more detailed usage, you can visit Thor github page and its rdoc
Summary
Of course there are more ways to write command-line tool. Choose what best fit your requirement rather than the most powerful or latest one.
All the sample code is on github https://github.com/allenwei/ruby_command_line_sample