Published 2023-10-03.
Last modified 2025-06-24.
Time to read: 6 minutes.
ruby
collection.
The Ruby language has many libraries for parsing command-line arguments.
This article discusses OptionParser
, which
is one of the most popular Ruby libraries for parsing arguments.
OptionParser
is popular because it is both powerful and easy to use,
and it is part of the Ruby runtime library.
This is the OptionParser
GitHub repository.
The F/OSS library called sod
enhances OptionParser
.
Although sod
can be used as an OptionParser
wrapper that provides enhanced capability,
you can also just use selected features of sod
while continuing to use OptionParser
as usual.
That is the approach taken in this article.
One sod
feature that I especially like is the
extra data types
that it supports for parsing arguments:
-
Pathname
returns aPathname
instance. -
Version
returns aVersionaire::Version
instance. - Custom parsers are possible that return arbitrary data types.
Installation
OptionParser
is part of the standard Ruby runtime library,
so once Ruby itself is installed, there are no mandatory additional steps for installation.
Gemfile
If you are building an application,
add the following lines to your application’s Gemfile
:
gem 'optparse' gem 'sod' # If you want extra datatypes
And then execute:
$ bundle
Gem
If you are building a gem,
add the following line to your gem’s .gemspec
:
spec.add_dependency 'optparse' spec.add_dependency 'sod' # If you want extra datatypes
And then execute:
$ bundle
Usage
My stabilize_video
program is an example of how I like to use OptionParser
.
Below are portions of three Ruby source files:
option.rb
parses the options and generates the help text.stabilize_video.rb
parses the mandatory arguments.stablize.rb
receives the mandatory arguments and options.
The following import is required before invoking creating an OptionParser
instance:
require 'optparse' require 'sod' # If you want to use sod require 'sod/types/pathname' # If you want to parse Pathnames
Parsing Options
The link to the detailed Ruby documentation for OptionParser
is broken.
You can read it
here.
Let’s look at an initial version of the parse_options
method,
which uses OptionParser
to parse the optional arguments.
Later in this article
I show a more flexible implementation that allows the parsing code to be testable.
def parse_options options = { shake: 5, loglevel: 'warning' } OptionParser.new do |parser| parser.program_name = File.basename __FILE__ @parser = parser parser.on('-f', '--overwrite', 'Overwrite output file if present') parser.on('-l', '--loglevel LOGLEVEL', Integer, "Logging level (#{VERBOSITY.join ', '})") parser.on('-s', '--shake SHAKE', Integer, 'Shakiness (1..10)') parser.on('-v', '--verbose VERBOSE', 'Verbosity') parser.on('-z', '--zoom ZOOM', Integer, 'Zoom percentage') parser.on_tail('-h', '--help', 'Show this message') do help end end.order!(into: options) help "Invalid verbosity value (#{options[:verbose]}), must be one of one of: #{VERBOSITY.join ', '}." if options[:verbose] && !options[:verbose] in VERBOSITY help "Invalid shake value (#{options[:shake]})." if options[:shake].negative? || options[:shake] > 10 options end
-
The default options are set in the highlighted hash.
Default values are set for the
:shake
and:loglevel
keys. -
When
parser.on
is passed the name of an option value in UPPER CASE, it creates an entry in theoptions
hash with that name, in lower case. The above code shows the following examples:-
LOGLEVEL
provides a means for the user to specify a value to replace the default value of theloglevel
entry in theoptions
hash, which was initialized with the string value'warning'
. -
SHAKE
provides a means for the user to specify a to replace the default value of theshake
entry in theoptions
hash, which was initialized with the integer value5
. -
VERBOSE
provides a means for the user to specify a string value for a new entry in theoptions
hash, with the keyverbose
. -
ZOOM
provides a means for the user to specify a string value for a new entry in theoptions
hash, with the keyzoom
.
-
-
OptionParser.
has the side effect that option keywords and key/value pairs that matchorder! parser.on
statements are removed fromARGV
. -
Ending the
end.order!
statement with(into: options)
causes the parsed option key/value pairs to be added or updated in the hash calledoptions
. -
The parsed
options
are returned.
Parsing Mandatory Arguments
Let’s see how stabilize_video.rb
parses the mandatory arguments:
require 'colorator'
require_relative 'stabilize_video/version'
require_relative 'options'
# Require all Ruby files in 'lib/', except this file
Dir[File.join(__dir__, '*.rb')].each do |file|
require file unless file.end_with?('/stabilize_video.rb')
end
def main
options = parse_options
help 'Video file name must be provided.' if ARGV.empty?
help "Too many parameters specified.\n#{ARGV}" if ARGV.length > 1
video_in = ARGV[0]
video_out = "#{File.dirname video_in}/stabilized_#{File.basename video_in}"
StablizeVideo.new(video_in, video_out, **options).stabilize
end
main
Here are some notes to help you understand the above code:
-
Usage of
colorator
orrainbow
to output colored strings helps readability, but is not required. -
Because
OptionParser
removes each argument fromARGV
that it recognizes, when it finishes all that should be left on the command line are the mandatory arguments. Themain
method callsparse_options
, which as we know callsOptionParser
, and then ensures that a mandatory filename parameter is provided. -
parse_options
returns a hash of name/value pairs, which can optionally be passed when doubly dereferenced with two asterisks (**options
). This is done in the highlighted code above when creating a newStablizeVideo
instance.
Passing Options
In the following code, the optional values returned by parse_options
are provided to the StablizeVideo.
method.
Once again, double asterisks are used.
def initialize(video_in, video_out, **options) @options = options @loglevel = "-loglevel #{options[:loglevel]}" @loglevel += ' -stats' unless options[:loglevel] == 'quiet' @shakiness = "shakiness=#{options[:shake]}" @video_in = MSUtil.expand_env video_in @video_out = MSUtil.expand_env video_out unless File.exist?(@video_in) printf "Error: file #{@video_in} does not exist.\n" exit 2 end unless File.readable? @video_in printf "Error: #{@video_in} cannot be read.\n" exit 2 end return unless File.exist?(@video_out) && !options.key?(:overwrite) printf "Error: #{@video_out} already exists.\n" exit 3 end
Notice in the above code that:
-
The value of the
-l
/--loglevel
option is obtained fromoptions[:loglevel]
. -
The value of the
-s
/--shake
option is obtained fromoptions[:shake]
. -
If the user specified the
-f
(--overwrite
) option, that is detected byoptions.key?(:overwrite)
.
Hand-written Help Text
I find that using the automatically generated help text results in a more complex program for little gain,
because there are so many moving parts to keep track of.
Explicitly writing the help
method is a more maintainable way of showing the user what they need to know.
The help
method below generates the help text, which might be preceded with an error message.
def help(msg = nil) printf "Error: #{msg}\n\n".yellow unless msg.nil? msg = <<~END_HELP stabilize: Stabilizes a video using the FFmpeg vidstabdetect and vidstabtransform filters. Syntax: stabilize [Options] PATH_TO_VIDEO Options: -f Overwrite output file if present -h Show this help message -s Shakiness compensation 1..10 (default 5) -v Verbosity; one of: #{VERBOSITY.join ', '} -z Zoom percentage (computed if not specified) See: https://www.ffmpeg.org/ffmpeg-filters.html#vidstabdetect-1 https://www.ffmpeg.org/ffmpeg-filters.html#toc-vidstabtransform-1 END_HELP printf msg.cyan exit 1 end
A Ruby squiggly heredoc is used to store a multiline string as the help text.
Running the Program
Now let's run the above program and view the generated help text:
$ stabilize -h stabilize -h stabilize: Stabilizes a video using the FFmpeg vidstabdetect and vidstabtransform filters. Syntax: stabilize [Options] PATH_TO_VIDEO Options: -f Overwrite output file if present -h Show this help message -s Shakiness compensation 1..10 (default 5) -v Verbosity; one of: trace, debug, verbose, info, warning, error, fatal, panic, quiet -z Zoom percentage (computed if not specified) See: https://www.ffmpeg.org/ffmpeg-filters.html#vidstabdetect-1 https://www.ffmpeg.org/ffmpeg-filters.html#toc-vidstabtransform-1
Testing Argument Parsing
As we have seen, Ruby passes an argument to the
OptionParser
block body.
As a reminder, the following code shows the block body and the argument, called parser
:
OptionParser.new do |parser| # OptionParser block body end
Designing Parsing Code for Testability
The argument provided to the OptionParser
block body, called parser
,
is an OptionParser
instance.
One of its attributes is default_argv
, which is a copy of the ARGV
array
the the operating system passes to your Ruby program when it starts.
You can assign a different array to be used instead of ARGV
by
assigning a value to the default_argv
attribute of the OptionParser
instance:
The following Ruby code defines a method called parse_options
that demonstrates a technique for
command-line option parsing.
Key features of this technique are:
- Default values can be established in a modular fashion
- Option parsing code is more modular, which makes it easier to understand
OptionParser
functionality is not affected- System-provided values can be overridden for testing
- Nothing extra required for full RSpec support
I intend to use this latest iteration of the technique in all my scripts and CLI tools.
require 'optparse'
def parse_options(default_options, argv_override: nil)
options = default_options
OptionParser.new do |parser|
parser.default_argv = argv_override if argv_override
# Remaining parameter parsing code goes here
# For example:
parser.on '-v', '--verbose', FalseClass
end.order! into: options
options
end
The parse_options
method accepts one or two arguments:
default_options
, which is a hash containing default values for options,
and the optional argv_override
argument,
which allows you to specify a custom array of arguments to parse instead
of the default system ARGV
provided by Ruby.
The parse_options
method body initializes a new automatic variable called options
variable from the default_options
parameter.
A new OptionParser
instance is then created called parser
.
It’s scope is only within the OptionParser
body.
The odd syntax of within this method body actually does the parsing of the arguments,
and then stores the results in the options
hash.
The highlighted line within the OptionParser
body is important:
this code tells the parser to use the optional argv_override
array, if provided,
as the source of command-line arguments.
This technique is useful for testing and for programmatically specifying arguments.
Define the options that your script accepts within the remainder of the parser
block.
The example recognizes a -v
or --verbose
flag.
After instantiating the OptionParser
instance called parser
,
the end
closes the OptionParser
constructor block, which finalizes the parsing process.
The order!
method is then called with an option called into:
,
followed by the name of the variable that will receive the hash resulting from the parsing operation that just completed.
Finally, the parse_options
method returns the populated options
hash.
It is up to the caller to handle any remaining arguments in ARGV
.
Testing The Parsing Code
The following example invocation demonstrates how to call parse_options
with a set of default options
and a custom argument String array, simulating command-line input.
The demonstrated approach shows how to make option parsing flexible and testable. You can easily override the arguments without going through the pain of providing actual command lines to running instances of your code, or getting frustrated with Visual Studio Code’s inability to accept ARGV tokens with embedded spaces.
The following example shows how to use the parse_options
method above.
You might write code like this for unit tests of CLI user invocation.
This example simulates a CLI being called with two positional parameters (param1
and param2
),
and without an optional parameter (-v
/ --verbose
) being provided.
irb(main):002* default_options = { irb(main):003* param1: 'value_1', irb(main):004* param2: 'value_2' irb(main):005> } => {:param1=>"value_1", :param2=>"value_2"}
irb(main):006> my_argv = %w[param1 value_3 param2 value_4] => ["param1", "value_3", "param2", "value_4"]
irb(main):006> parse_options default_options, argv_override: my_argv {:param1=>"value_1", :param2=>"value_2"}
You can write RSpec unit tests that incorporate similar code. This allows you to test CLI argument parsing easily.
Highline
Highline
is a gem that is often found in CLIs that are built with OptionParser
.
It provides a way to ask the user questions and get answers,
similar in syntax to methods that Thor provides for the same purpose.
The agree
method
is particularly useful.
Both the agree
and
ask
methods
have an undocumented feature:
Putting a space at the end of the question string suppresses the newline between the question and the answer.
The optional character
parameter causes the first character that the user types to be immediately
processed without requiring them to press Enter.
$ irb irb(main):001> require 'highline' => true irb(main):002* begin irb(main):003* printf "Work work work" irb(main):004> end while HighLine.agree "\nAll done! Do you want to do it again? ", character = true Work work work All done! Do you want to do it again? Please enter "yes" or "no". All done! Do you want to do it again? y Work work work All done! Do you want to do it again? y Work work work All done! Do you want to do it again? n => nil irb(main):005>
Below is some code that I wrote for
Nugem.
The import
statement
imports the methods of the Highline library directly into the current namespace.
require 'highline/import' module HighlineWrappers def yes_no?(prompt = 'More?', default_value: true) answer_letter = '' suffix = default_value ? '[Y/n]' : '[y/N]' default_letter = default_value ? 'y' : 'n' until %w[y n].include? answer_letter # rubocop:disable Performance/CollectionLiteralInLoop answer_letter = ask("#{prompt} #{suffix} ") do |q| q.limit = 1 q.case = :downcase end answer_letter = default_letter if answer_letter.empty? end answer_letter == 'y' end # Invokes yes_no? with the default answer being 'no' def no?(prompt = 'More?') yes_no? prompt, default: false end # Invokes yes_no? with the default answer being 'yes' def yes?(prompt = 'More?') yes_no? prompt, default: true end end
The above code can be mixed into any class or module.
They ask the user a question and return true
or false
based on the user’s response.
The methods in the code issue a prompt and then read just one character from the user. The user can respond by typing the single character y to answer “yes”, or n to answer “no”. If the user presses Space or Enter without typing anything else, the default answer is used.
The following irb
session shows how to use the yes?
and no?
methods.
irb(main):152> yes? 'asdf' asdf [Y/n] Enter or Space => true
irb(main):153> yes? 'asdf' asdf [Y/n] n => false
irb(main):154> no? 'asdf' asdf [y/N] Enter or Space => false
irb(main):155> no? 'asdf' asdf [y/N] n => false
irb(main):156> no? 'asdf' asdf [y/N] y => true