Building Ruby CLIs with OptionParser
Introduction
Every Ruby script that accepts user input from the command line needs a way to handle arguments like -f filename or --verbose. Building Ruby command-line tools starts with OptionParser, a standard library class that ships with Ruby and handles argument parsing cleanly.
OptionParser handles the boring parts for you. It automatically generates help text, coerces arguments into the right types, and raises helpful error messages when something goes wrong. This guide walks you through everything you need to build a polished CLI in Ruby.
Key takeaways
OptionParseris part of Ruby’s standard library, so you can use it without adding a gem.- The parser can generate help output, validate arguments, and coerce values into common Ruby types.
- Block-based parsing gives you the most control when each option needs custom handling.
into:is a concise way to collect parsed values into a hash.- Custom coercion keeps option validation close to the parser instead of scattering it through the rest of the script.
If you are building a small command-line tool, OptionParser often gives you enough structure without pulling in another dependency. If the tool grows, the parser still scales well because the argument contract stays in one obvious place.
Setting up a parser for building Ruby CLIs
The first step is to require the library and create an OptionParser instance. You configure the parser by passing a block, where you set the banner and define your options.
require 'optparse'
options = {}
parser = OptionParser.new do |opts|
opts.banner = "Usage: myapp.rb [options]"
end
parser.parse!
Running this script with -h or --help prints the banner. The banner text is exactly what you set in opts.banner, which means you can customize it to show your script’s name, version, or any other context that helps users understand what the command does.
Usage: myapp.rb [options]
By itself the parser does not do much because no options have been registered yet. The next step is to call on inside the block to define the flags your CLI accepts. Each option definition tells the parser what flag to watch for and what to do with the value.
That first parser object is the foundation for everything else in this guide. Once you know where the banner lives and how options are registered, the remaining pieces mostly add new shapes of arguments without changing the overall flow.
Defining your first option
You define options inside the block using the on method. Each call to on specifies the short flag, long flag, an optional description, and a block that receives the parsed value.
require 'optparse'
options = {}
parser = OptionParser.new do |opts|
opts.banner = "Usage: myapp.rb [options]"
opts.on("-n", "--name NAME", "Your name") do |name|
options[:name] = name
end
end
parser.parse!
p options
Try running the script to see how the parser handles a named argument. The output is a hash with the :name key set to the value you passed on the command line, confirming that the block assigned it correctly to the options hash.
$ ruby myapp.rb --name Alice
{:name=>"Alice"}
The --name NAME syntax tells OptionParser that NAME is a required argument. If a user runs the script without providing a value, OptionParser raises OptionParser::MissingArgument. The error message includes the option name so the user knows exactly what is missing.
That behaviour is useful because it keeps validation close to the parser instead of forcing you to inspect raw ARGV manually later in the script. Your code can assume the value exists once parsing succeeds.
Boolean flags
Many CLI tools have flags that simply toggle a setting on or off, like --verbose or --quiet. OptionParser supports this with a special syntax that creates both --verbose and --no-verbose flags from a single option definition. The [no-] prefix is all you need.
require 'optparse'
options = {}
parser = OptionParser.new do |opts|
opts.on("--[no-]verbose", "Enable verbose output") do |v|
options[:verbose] = v
end
end
parser.parse!
p options
Running this demonstrates the paired flags that OptionParser created from the single [no-] definition. Notice how the same option definition responds to both forms, setting the value to true or false depending on which flag the user typed. No additional code is needed for the negation.
$ ruby myapp.rb --verbose
{:verbose=>true}
$ ruby myapp.rb --no-verbose
{:verbose=>false}
The [no-] prefix is the shorthand for this pattern. OptionParser expands it into two separate flags automatically. This is one of the reasons OptionParser feels pleasant in real scripts: a single option definition gives you both sides of the toggle, and the resulting API is easy for users to remember. No extra code is needed to handle the negation case.
Type coercion
One of OptionParser’s most useful features is automatic type coercion. Instead of receiving everything as a string, you can specify the type and OptionParser converts it for you. This eliminates manual casting after parsing and catches type errors early.
require 'optparse'
options = {}
parser = OptionParser.new do |opts|
opts.on("-p", "--port PORT", Integer, "Port number") do |port|
options[:port] = port
end
opts.on("-t", "--tags TAG1,TAG2", Array, "Comma-separated tags") do |tags|
options[:tags] = tags
end
end
parser.parse!
p options
Coercion works for several built-in types. The output shows how the -p flag returns an Integer instead of a string, and the -t flag splitting a comma-separated value into an Array automatically. No manual parsing is needed after the fact.
$ ruby myapp.rb -p 8080
{:port=>8080, :tags=>nil}
$ ruby myapp.rb -t foo,bar,baz
{:port=>nil, :tags=>["foo", "bar", "baz"]}
The available built-in converters include Integer, Float, String, Array, Date, URI, and Regexp. For non-numeric ports like -p hello, OptionParser raises an InvalidArgument error before your code ever sees the value. You can also restrict values to a specific list by passing an array of allowed strings instead of a type class.
require 'optparse'
options = {}
parser = OptionParser.new do |opts|
opts.on("--format FORMAT", %w[json yaml xml], "Output format") do |fmt|
options[:format] = fmt
end
end
parser.parse!
p options
Passing an invalid value raises OptionParser::InvalidArgument, which means your code never sees a bad value. The error fires during parsing, before the options hash is populated, so downstream code can safely assume the values it reads are valid without additional checks.
$ ruby myapp.rb --format csv
# => OptionParser::InvalidArgument: invalid argument: --format=csv
Type coercion makes a CLI feel polished because the parser returns the data in the shape your code actually wants. That means less manual conversion after parsing and fewer chances to forget a numeric cast or a list split somewhere else in the script. The list-constraint variant is especially handy for flags that can only accept a few known values, like --format or --log-level.
Common patterns for real-world CLIs
- Start with a hash of defaults so the script has sensible values before parsing.
- Keep the parser setup near the top of the file so the interface is easy to scan.
- Let
OptionParserhandle help output so the usage text stays in sync with the actual options. - Validate any domain-specific rules immediately after parsing, while the options are still fresh in your head.
Optional arguments
By default, arguments like NAME in --name NAME are required. To make an argument optional, use square brackets around it. This is useful for flags where omitting the value means “use the default” rather than “error out.”
require 'optparse'
options = {}
parser = OptionParser.new do |opts|
opts.on("-c", "--count [N]", Integer, "Count of items") do |n|
options[:count] = n
end
end
parser.parse!
p options
Optional arguments can be omitted entirely. The first invocation shows that -c alone sets :count to nil rather than raising an error. The second invocation passes a value and gets the normal integer coercion. This flexibility means one flag can serve two purposes in a single option definition.
$ ruby myapp.rb -c
{:count=>nil}
$ ruby myapp.rb -c 5
{:count=>5}
Optional values are handy for commands where a flag can mean “use the default” or “use the supplied value.” They keep the CLI compact without forcing the user to learn a second option name for the same idea. The downside is that users may not realize the value is optional, so a good description in the help text helps clarify the behaviour.
Generating help
Good CLI tools include help text. OptionParser makes this straightforward. Add a -h and --help option that prints the parser itself and exits.
require 'optparse'
options = {}
parser = OptionParser.new do |opts|
opts.banner = "Usage: myapp.rb [options]"
opts.on("-n", "--name NAME", String, "Your name") do |name|
options[:name] = name
end
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
options[:verbose] = v
end
opts.on("-h", "--help", "Show this help") do
puts opts
exit
end
end
parser.parse!
puts "Running with #{options}"
Running ruby myapp.rb --help produces formatted help text with each option’s short flag, long flag, and description aligned in columns. OptionParser builds this output automatically from the on definitions, so the help text always reflects the actual options that the parser accepts.
Usage: myapp.rb [options]
-n, --name NAME Your name
-v, --[no-]verbose Run verbosely
-h, --help Show this help
You can also use parser.separator to group related options visually, and on_tail to list options at the bottom of the help text. Separators add blank lines and section labels that break long option lists into scannable groups.
Help output is one of the biggest quality-of-life wins for a CLI. If the parser can explain itself clearly, users can often discover the right usage without opening the source code or the README. That matters because most users try --help before they read documentation.
Using into:
OptionParser also supports a cleaner approach with the into: parameter, which stores all parsed options directly into a hash without needing individual blocks for each option. This is the fastest way to get a working parser when you do not need per-option validation or transformation.
require 'optparse'
options = {}
parser = OptionParser.new do |p|
p.on("-n", "--name NAME", "Your name")
p.on("-c", "--count N", Integer, "Count")
p.on("-v", "--[no-]verbose", "Verbose mode")
end
parser.parse!(into: options)
p options
The into: parameter tells OptionParser to store each parsed value directly into the provided hash, using the long option name as the key. You can see how all three flags populate the hash in a single call without any explicit blocks for individual option handling.
$ ruby myapp.rb --name Alice -c 3 --verbose
{:name=>"Alice", :count=>3, :verbose=>true}
The trade-off is that into: gives you less control over each individual option. Use the block style when you need to validate or transform values during parsing. Use into: when you just want to collect values into a hash without any extra processing.
That tradeoff shows up in a lot of Ruby APIs: a shorter interface is easier to read, while a more explicit one gives you more room to shape the data on the way in. The right choice depends on whether your options are simple flags and values or whether each option needs its own validation logic.
Choosing a parsing method
OptionParser provides several parsing methods with different behaviour. The right one depends on whether your CLI accepts mixed arguments and options or expects options first followed by positional arguments.
parse!(ARGV)— parses all arguments, raises on unknown optionsparse(ARGV)— same asparse!but leavesARGVunchangedorder!(ARGV)— parses in order, stops at the first non-option argumentpermute!(ARGV)— allows options and arguments to be mixed freely
require 'optparse'
options = {}
parser = OptionParser.new do |p|
p.on("-n", "--name NAME")
p.on("-v", "--[no-]verbose")
end
# order! stops at first non-option, useful for subcommand-style CLIs
args = ARGV.select { |arg| arg.start_with?("-") }
parser.order!(into: options)
puts "Options: #{options}"
puts "Remaining: #{ARGV}"
The parsing method you choose should match the user experience you want. Strict parsing is good for small utilities that should fail early on unknown input. Ordered or permissive parsing is better when the script accepts both options and positional arguments, like git subcommands.
$ ruby myapp.rb --name Alice git push
Options: {:name=>"Alice"}
Remaining: ["git", "push"]
This pattern is common in CLI tools that wrap other commands. The options configure the wrapper’s behaviour, and the remaining arguments get forwarded to the subcommand unchanged.
Custom type coercion
For types beyond the built-in converters, you can define your own using accept. This is useful when you want to parse a custom format or validate input during parsing instead of deferring validation to later code.
require 'optparse'
Range = Struct.new(:min, :max)
options = {}
parser = OptionParser.new do |p|
p.accept(Range) do |str|
min, max = str.split("-").map(&:to_i)
Range.new(min, max)
end
p.on("--range RANGE", Range, "A range like 1-10") do |range|
options[:range] = range
end
end
parser.parse!(into: options)
p options
The accept block splits the input string on the hyphen, converts both parts to integers, and returns a Range struct. When the parser encounters --range 5-20, it passes “5-20” through the accept block before assigning it to options, so the hash value is already structured correctly.
$ ruby myapp.rb --range 5-20
{:range=>#<struct Range min=5, max=20>}
Custom coercion is useful when the raw string is not the right shape for the rest of your code. You can convert, validate, and reject bad input in one place instead of spreading those checks across the rest of the command. The accept block runs before the option block, so the value your block receives is already structured correctly.
Handling parse errors
When something goes wrong, OptionParser raises specific exceptions. You can catch these to provide a clear error message instead of a raw stack trace. This is the difference between a CLI that feels polished and one that feels like a prototype.
require 'optparse'
options = {}
parser = OptionParser.new do |p|
p.on("-p", "--port PORT", Integer, "Port number")
p.on("-h", "--help", "Show help") do
puts p
exit
end
end
begin
parser.parse!
rescue OptionParser::InvalidOption => e
puts "Unknown option: #{e.message}"
exit 1
rescue OptionParser::MissingArgument => e
puts "Missing value for #{e.message}"
exit 1
rescue OptionParser::InvalidArgument => e
puts "Invalid value: #{e.message}"
exit 1
end
The begin/rescue block wraps parser.parse! and catches the three most common parsing exceptions. Each rescue branch prints a message that tells the user what went wrong, then exits with a non-zero status code so calling scripts can detect the failure.
$ ruby myapp.rb -p hello
Invalid value: invalid number: hello
Parsing errors are worth catching because they let you give the user a direct, actionable message. Instead of a stack trace with Ruby internals, the CLI can tell the user which option was wrong and how to fix it. Each exception type maps to a specific user mistake, so the error messages can be genuinely helpful.
A complete example
Putting it all together, here is a greeting script with named greetings, a count option, verbose mode, and help text. This example pulls together defaults, validation, help text, and a small amount of business logic after parsing, showing the real shape of a small Ruby CLI.
#!/usr/bin/env ruby
require 'optparse'
options = {
name: "world",
count: 1,
verbose: false
}
OptionParser.new do |parser|
parser.banner = "Usage: greet.rb [options]"
parser.on("-n", "--name NAME", String, "Name to greet") do |name|
options[:name] = name
end
parser.on("-c", "--count N", Integer, "Number of greetings") do |n|
options[:count] = n
end
parser.on("-v", "--[no-]verbose", "Enable verbose mode") do |v|
options[:verbose] = v
end
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
end.parse!
options[:count].times do
puts "Hello, #{options[:name]}!"
puts "(running in verbose mode)" if options[:verbose]
end
The complete example shows the real shape of a small Ruby CLI. Running it with --help prints the formatted usage, and running it with actual arguments demonstrates how the options hash flows into the business logic at the end.
$ ruby greet.rb --help
Usage: greet.rb [options]
-n, --name NAME Name to greet
-c, --count N Number of greetings
-v, --[no-]verbose Enable verbose mode
-h, --help Show this help
$ ruby greet.rb -n Alice -c 3 -v
Hello, Alice!
(running in verbose mode)
Hello, Alice!
(running in verbose mode)
Hello, Alice!
(running in verbose mode)
The parser defines the contract, the defaults keep the script predictable, and the loop at the end focuses on the actual work instead of argument handling. This separation makes the CLI easy to extend: add a new option to the parser block and a new behaviour after parse!, and the two concerns stay clearly separated.
Frequently asked questions
Is OptionParser good enough for production CLIs?
Yes. It is a standard library tool and it handles the common cases well. If your CLI grows into a large application with subcommands and plugins, you might eventually want something more specialized like Thor, but OptionParser is a solid starting point that you can build on incrementally.
Should I use into: or blocks?
Use blocks when you need custom validation or transformation per option. Use into: when you want a concise parser that just collects values into a hash. The into: style is shorter but gives you less control over individual values.
How do I make help text more useful?
Give each option a short, concrete description and group related options together with separators. A good help screen should explain the purpose of the command without forcing the user to read the source.
Why building Ruby CLIs with OptionParser works
Building Ruby CLIs with OptionParser feels straightforward because the library handles the repetitive parts of argument parsing for you. The parser can collect values, coerce types, generate help text, and raise clear errors when input is wrong. That leaves you free to focus on the command’s actual job.
If you keep the interface small and the option names clear, the script stays easy to use and easy to maintain. That is the real win when building Ruby tools: a command-line interface that tells the user what it expects before they hit enter.
See Also
- Working with Strings in Ruby — strings come up constantly in CLI work for formatting output and parsing arguments
- Ruby Regular Expressions — when OptionParser is not enough for complex argument patterns, regex fills the gaps
- Ruby Blocks, Procs, and Lambdas — understanding closures helps when writing custom OptionParser blocks and coercions
- Ruby Command Pattern — a design pattern that pairs naturally with CLI tools for organizing subcommands
- Ruby IO and Files — most CLI tools read input or write output to files
- OptionParser docs (ruby-doc.org)