CLI Scripts with OptionParser & ARGV
Reviewed & published by Brayan K
A command-line script reads its arguments from the ARGV array, and Ruby's standard-library OptionParser turns raw flags into a clean options hash with validation and help text built in.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll build a real CLI tool that parses flags, coerces types, handles errors, and returns proper exit codes.
What You'll Learn in This Lesson
- Reading command-line args from ARGV
- Parsing flags with OptionParser
- Banners, short/long flags, and required values
- Coercing values to Integer and other types
- Error handling and exit codes
- Reading interactive input with gets
1️⃣ ARGV: The Raw Arguments
Every Ruby script gets its command-line arguments in the built-in ARGV array, already split by the shell. For simple tools you can read it directly — separating flags from positional values by hand — but this gets brittle fast, which is exactly why OptionParser exists.
# ARGV is an array of the command-line arguments passed to your script.
# Running: ruby greet.rb Ada Lee --loud
# gives ARGV == ["Ada", "Lee", "--loud"]. We'll simulate it here:
argv = ["Ada", "Lee", "--loud"]
flags = argv.select { |a| a.start_with?("--") } # ["--loud"]
names = argv.reject { |a| a.start_with?("--") } # ["Ada", "Lee"]
names.each do |name|
greeting = "Hello, #{name}"
greeting = greeting.upcase + "!" if flags.include?("--loud")
puts greeting
end2️⃣ OptionParser: Flags into a Hash
Require 'optparse', build an OptionParser, and register each flag with opts.on(...). Pass a type like Integer and OptionParser coerces the value for you; set defaults on the hash beforehand. Then parse! fills the hash from the arguments.
require 'optparse'
# OptionParser parses flags into an options hash, with help text built in.
options = { verbose: false, count: 1 } # sensible defaults
parser = OptionParser.new do |opts|
opts.banner = "Usage: greet.rb [options] NAME"
# A flag with a required value (the String forces a value):
opts.on("-nNAME", "--name=NAME", String, "Who to greet") do |name|
options[:name] = name
end
# An option that takes an Integer value:
opts.on("-cN", "--count=N", Integer, "How many times") do |n|
options[:count] = n
end
# A simple boolean switch (no value):
opts.on("-v", "--verbose", "Print extra info") do
options[:verbose] = true
end
end
# Normally OptionParser reads ARGV; here we parse a hardcoded array.
parser.parse!(["--name=Ada", "--count=3", "--verbose"])
p options3️⃣ Error Handling & Exit Codes
A robust CLI validates its input. OptionParser raises OptionParser::InvalidOption on unknown flags; rescue it, print a message to stderr with warn, and exit with a non-zero code so callers know it failed. A successful run should exit 0.
require 'optparse'
# Exit codes communicate success (0) or failure (non-zero) to the shell.
# OptionParser raises on bad input; we rescue, warn, and exit with a code.
def parse(argv)
options = {}
parser = OptionParser.new do |opts|
opts.on("--port=PORT", Integer, "Port number") { |p| options[:port] = p }
opts.on("-h", "--help", "Show help") { puts opts; exit 0 }
end
parser.parse!(argv)
options
rescue OptionParser::InvalidOption => e
warn "Error: #{e.message}"
return :error
end
# Good input parses fine:
p parse(["--port=8080"])
# Unknown flag triggers the rescue branch:
result = parse(["--bogus"])
puts "Exit code would be: #{result == :error ? 1 : 0}"🎯 Your Turn
The --upcase switch is declared but stores into the wrong key. Replace the ___ with the key set up in the defaults hash.
require 'optparse'
# 🎯 YOUR TURN — fill in the ___ so --upcase is parsed into the hash.
options = { upcase: false }
OptionParser.new do |opts|
opts.on("-u", "--upcase", "Shout the message") do
options[:___] = true # 👉 which key did we declare above?
end
end.parse!(["--upcase"])
message = "ready"
message = message.upcase if options[:upcase]
puts message
# ✅ Expected output:
# READYCommon Errors (and the fix)
- "uninitialized constant OptionParser" — ❌ You forgot the require. ✅ Add require 'optparse' at the top.
- "invalid option: --foo" — ❌ A flag you didn't register. ✅ Declare it with opts.on or rescue the error.
- "missing argument: --name" — ❌ A value-taking flag got none. ✅ Pass --name=Ada or rescue MissingArgument.
- Value is a String, not a number — ❌ No type given. ✅ Add Integer in the opts.on call.
- Positional args vanished — ❌ parse! consumed them. ✅ Read what's LEFT in the array after parsing — that's your positionals.
Pro Tips
- 💡 Restrict values: pass an array like %w[low normal high] to opts.on to reject anything else.
- 💡 Repeatable flags: push into an array inside the block so --tag a --tag b collects both.
- 💡 Always add --help: opts.on("-h", "--help") { puts opts; exit } gives users a free manual.
Mini-Challenge: A Tasker CLI Parser
Parse a task-creation argv with a restricted --priority, a repeatable --tag, a --done switch, and a positional title. Run with ruby tasker.rb.
# 🎯 MINI-CHALLENGE: a tiny "tasker" CLI parser.
# Save as tasker.rb and run with: ruby tasker.rb
#
# Parse this argv with OptionParser into an options hash:
# ["--priority=high", "--tag=work", "--tag=urgent", "--done", "Ship", "release"]
#
# 1. --priority accepts only low/normal/high (default "normal").
# 2. --tag is REPEATABLE: collect each into an array.
# 3. --done is a boolean switch.
# 4. Whatever is left in argv after parsing becomes the joined :title.
#
# ✅ Expected output:
# {:priority=>"high", :tags=>["work", "urgent"], :done=>true, :title=>"Ship release"}
# your code here📋 Quick Reference — CLI & OptionParser
| Goal | Ruby |
|---|---|
| Raw arguments | ARGV |
| Load the parser | require 'optparse' |
| Boolean switch | opts.on("-v", "--verbose") |
| Typed value | opts.on("--count=N", Integer) |
| Parse & consume | parser.parse!(ARGV) |
| Signal failure | warn msg; exit 1 |
| Read input | gets.chomp |
🎉 Lesson Complete!
- ✅ ARGV holds your script's command-line arguments
- ✅ require 'optparse' then opts.on registers flags
- ✅ Pass a type (e.g. Integer) to coerce values automatically
- ✅ parse! fills the hash and leaves positionals behind
- ✅ Rescue bad flags, warn, and exit with a code
Practice quiz
What is ARGV in a Ruby script?
- The script's filename
- A hash of environment variables
- An array of the command-line arguments passed to the script
- The standard input stream
Answer: An array of the command-line arguments passed to the script. ARGV is a built-in array holding the command-line arguments, already split by the shell.
Which require loads OptionParser?
- require 'optparse'
- require 'optionparser'
- require 'argv'
- require 'options'
Answer: require 'optparse'. OptionParser lives in 'optparse' — require 'optparse' before using it.
Running ruby greet.rb Ada --loud gives ARGV equal to:
- ["greet.rb", "Ada", "--loud"]
- ["Ada"]
- ["--loud"]
- ["Ada", "--loud"]
Answer: ["Ada", "--loud"]. The script name is NOT in ARGV (that's $0); ARGV is ["Ada", "--loud"].
Passing Integer in opts.on("--count=N", Integer) does what?
- Limits the value to one digit
- Coerces the parsed value to an Integer automatically
- Requires the flag to be named Integer
- Nothing — it is ignored
Answer: Coerces the parsed value to an Integer automatically. Giving a type like Integer makes OptionParser coerce the flag's value to that type.
What is the difference between parse and parse!?
- parse! mutates the array, removing recognized flags and leaving positionals behind
- parse! is invalid
- parse! is faster but identical in behavior
- parse raises errors, parse! never does
Answer: parse! mutates the array, removing recognized flags and leaving positionals behind. parse! consumes recognized flags from the array, leaving the positional arguments behind.
Which exception does OptionParser raise on an unknown flag like --bogus?
- ArgumentError
- NoMethodError
- OptionParser::InvalidOption
- OptionParser::UnknownFlag
Answer: OptionParser::InvalidOption. An unregistered flag raises OptionParser::InvalidOption, which you can rescue.
In shell convention, what exit code signals SUCCESS?
- 1
- 0
- -1
- Any non-zero value
Answer: 0. Exit code 0 means success; any non-zero value signals an error to the shell.
After parse! consumes the flags, where are the positional arguments (like a title)?
- Lost forever
- In ENV
- In the options hash under :positional
- Whatever remains in the argv array after parsing
Answer: Whatever remains in the argv array after parsing. parse! leaves non-flag positional arguments behind in the array — read what's left.
How do you read one line of interactive input and strip the trailing newline?
- gets
- gets.chomp
- read.line
- input.next
Answer: gets.chomp. gets reads a line (including the newline); .chomp removes that trailing newline.
To collect a REPEATABLE flag like --tag a --tag b into an array, you should:
- Use a type of Array in opts.on
- It's impossible with OptionParser
- Push each value into an array inside the opts.on block
- Use parse instead of parse!
Answer: Push each value into an array inside the opts.on block. Push each parsed value into an array inside the block so repeated flags accumulate.
Continue this course
- Previous: Automation with Rake
- Next: Benchmarking & Profiling — Compare approaches and find hot spots with the Benchmark module
- Quick reference: Ruby cheat sheet
Frequently asked questions
What is ARGV in a Ruby script?
ARGV is a built-in array holding the command-line arguments passed to your script, already split on spaces by the shell. If you run ruby greet.rb Ada --loud, then ARGV is ['Ada', '--loud']. The script name itself is not in ARGV — that's available separately as $0. You can read ARGV directly for very simple scripts, but as soon as you have flags with values, optional switches, or help text, a dedicated parser like OptionParser saves you from fragile manual string-matching.
Why use OptionParser instead of parsing ARGV by hand?
OptionParser, from Ruby's standard library, handles all the fiddly parts of command-line parsing for you: long and short flag forms (--count and -c), required values with automatic type coercion to Integer or other types, repeatable options, sensible defaults, and a generated --help banner. It also validates input and raises clear errors on unknown or malformed flags. Writing the same robustness by hand with ARGV string matching is error-prone and verbose, so OptionParser is the standard, no-dependency choice.
How do exit codes work and why do they matter?
When a script finishes, it returns an exit code to the shell: 0 means success, any non-zero value signals an error. You set it with exit(code) or exit!(code), and a raised, uncaught exception exits non-zero automatically. Exit codes matter because other programs — shell scripts, CI pipelines, Makefiles — branch on them: command && next_step only runs next_step if command exited 0. A well-behaved CLI tool returns 0 on success and a meaningful non-zero code on failure so it composes cleanly.
How do I read interactive input with gets?
gets reads one line of input from standard input, including the trailing newline, returning nil at end-of-file. You typically call .chomp to strip that newline: name = gets.chomp. This is how a script prompts the user mid-run — print a question, then read the answer. Note a subtlety: if ARGV is non-empty, plain gets tries to read from files named in ARGV instead of the keyboard, so for interactive prompts use $stdin.gets to be explicit about reading from the terminal.