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

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
end

2️⃣ 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 options

3️⃣ 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:
#    READY

Common Errors (and the fix)

Pro Tips

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

GoalRuby
Raw argumentsARGV
Load the parserrequire 'optparse'
Boolean switchopts.on("-v", "--verbose")
Typed valueopts.on("--count=N", Integer)
Parse & consumeparser.parse!(ARGV)
Signal failurewarn msg; exit 1
Read inputgets.chomp

🎉 Lesson Complete!

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

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.