CrackedRuby logo

CrackedRuby

OptionParser

Overview

OptionParser provides Ruby applications with command-line argument processing capabilities. The library transforms command-line arguments into structured data, handles option validation, and generates help text. OptionParser supports short and long option formats, required and optional arguments, and automatic type conversion.

The primary class OptionParser acts as a parser builder and processor. Applications define options using the #on method, which associates option patterns with processing blocks. The parser processes ARGV or custom argument arrays, executing the appropriate blocks for matched options.

require 'optparse'

options = {}
parser = OptionParser.new do |opts|
  opts.banner = "Usage: myapp [options]"
  
  opts.on("-v", "--verbose", "Run verbosely") do |v|
    options[:verbose] = v
  end
end

parser.parse!(ARGV)
puts "Verbose mode: #{options[:verbose]}"

OptionParser handles three primary option types: flags (boolean switches), options with required arguments, and options with optional arguments. The library automatically converts arguments to appropriate Ruby types based on the option definition.

parser = OptionParser.new do |opts|
  opts.on("-p", "--port PORT", Integer, "Port number") do |port|
    options[:port] = port
  end
  
  opts.on("-f", "--file [FILE]", "Optional file") do |file|
    options[:file] = file || "default.txt"
  end
end

The parser distinguishes between destructive and non-destructive parsing methods. #parse! modifies the original argument array by removing processed options, while #parse returns a new array with unprocessed arguments.

Basic Usage

OptionParser construction begins with creating a parser instance and defining options through the #on method. Each option definition specifies the option format, argument requirements, and processing behavior.

require 'optparse'

options = {}
OptionParser.new do |parser|
  parser.banner = "Usage: backup.rb [options] directory"
  
  parser.on("-c", "--compress", "Enable compression") do
    options[:compress] = true
  end
  
  parser.on("-o", "--output FILE", "Output file") do |file|
    options[:output] = file
  end
  
  parser.on("-l", "--level LEVEL", Integer, "Compression level (1-9)") do |level|
    options[:level] = level
  end
end.parse!

Short options use single dashes followed by single characters, while long options use double dashes followed by words. OptionParser accepts both formats simultaneously for the same option. Arguments can be required (no brackets) or optional (enclosed in brackets).

The processing block receives the parsed argument value. For flag options without arguments, the block receives true. For options with arguments, the block receives the argument string or converted value based on the specified type.

options = {}
parser = OptionParser.new do |opts|
  # Flag option - no argument
  opts.on("-q", "--quiet") { options[:quiet] = true }
  
  # Required argument
  opts.on("-u", "--user USER") { |user| options[:user] = user }
  
  # Optional argument with default
  opts.on("-t", "--timeout [SECONDS]") do |timeout|
    options[:timeout] = timeout || "30"
  end
end

parser.parse!(["-q", "-u", "admin", "-t", "60"])
# options = { quiet: true, user: "admin", timeout: "60" }

OptionParser supports automatic type conversion by specifying a class or conversion object as the third argument to #on. Common types include Integer, Float, Array, and custom conversion patterns.

options = {}
OptionParser.new do |opts|
  opts.on("-p", "--port PORT", Integer) { |p| options[:port] = p }
  opts.on("-r", "--ratio RATIO", Float) { |r| options[:ratio] = r }
  opts.on("-i", "--include DIRS", Array) { |dirs| options[:include] = dirs }
  
  # Custom pattern matching
  opts.on("-m", "--mode MODE", %w[read write append]) do |mode|
    options[:mode] = mode
  end
end

parser.parse!(["-p", "8080", "-r", "1.5", "-i", "a,b,c", "-m", "write"])
# options = { port: 8080, ratio: 1.5, include: ["a", "b", "c"], mode: "write" }

Help text generation occurs automatically based on option definitions. The banner attribute sets the usage line, while option descriptions appear in the help output. OptionParser generates formatted help text showing all defined options and their descriptions.

parser = OptionParser.new do |opts|
  opts.banner = "Usage: deploy.rb [options] environment"
  opts.version = "1.2.0"
  
  opts.on("-d", "--debug", "Enable debug mode") { }
  opts.on("-c", "--config FILE", "Configuration file") { }
  
  opts.on_tail("-h", "--help", "Show this help") do
    puts opts
    exit
  end
  
  opts.on_tail("--version", "Show version") do
    puts opts.ver
    exit
  end
end

Error Handling & Debugging

OptionParser raises specific exceptions for different error conditions during argument parsing. Understanding these exceptions enables proper error handling and user feedback in command-line applications.

OptionParser::InvalidOption occurs when the parser encounters an unrecognized option. This exception includes the problematic option in its message and provides access to the invalid option through the #args method.

options = {}
parser = OptionParser.new do |opts|
  opts.on("-v", "--verbose") { options[:verbose] = true }
end

begin
  parser.parse!(["-x", "-v"])
rescue OptionParser::InvalidOption => e
  puts "Unknown option: #{e}"
  puts "Available options:"
  puts parser.help
  exit 1
end

OptionParser::MissingArgument signals that an option requiring an argument was provided without one. This commonly occurs when users specify options at the end of the command line or follow them with other options instead of arguments.

begin
  parser.parse!(["-o"])  # Missing required argument
rescue OptionParser::MissingArgument => e
  puts "Option #{e.args.first} requires an argument"
  exit 1
end

Type conversion failures raise OptionParser::InvalidArgument when the provided argument cannot be converted to the specified type. This exception occurs during automatic type conversion for Integer, Float, and custom type specifications.

parser = OptionParser.new do |opts|
  opts.on("-p", "--port PORT", Integer) { |p| options[:port] = p }
end

begin
  parser.parse!(["-p", "not_a_number"])
rescue OptionParser::InvalidArgument => e
  puts "Invalid argument: #{e}"
  puts "Port must be a number"
  exit 1
end

Comprehensive error handling wraps the parsing operation and provides specific feedback for each error type. This approach improves user experience by offering clear error messages and usage guidance.

def parse_options(args)
  options = {}
  parser = OptionParser.new do |opts|
    opts.banner = "Usage: processor.rb [options] files..."
    
    opts.on("-w", "--workers COUNT", Integer, "Worker threads") do |count|
      options[:workers] = count
    end
    
    opts.on("-f", "--format FORMAT", %w[json xml csv], "Output format") do |format|
      options[:format] = format
    end
  end
  
  begin
    remaining = parser.parse(args)
    if remaining.empty?
      raise "No input files specified"
    end
    [options, remaining]
  rescue OptionParser::InvalidOption => e
    $stderr.puts "Error: #{e}"
    $stderr.puts parser.help
    exit 1
  rescue OptionParser::MissingArgument => e
    $stderr.puts "Error: #{e}"
    exit 1
  rescue OptionParser::InvalidArgument => e
    $stderr.puts "Error: #{e}"
    exit 1
  rescue => e
    $stderr.puts "Error: #{e}"
    exit 1
  end
end

Debugging option parsing issues often requires examining the argument array at different stages. Adding debug output shows how OptionParser processes arguments and identifies parsing problems.

parser = OptionParser.new do |opts|
  opts.on("-d", "--debug") { options[:debug] = true }
  opts.on("-v", "--verbose") { options[:verbose] = true }
end

puts "Original args: #{ARGV.inspect}"
parser.parse!
puts "Remaining args: #{ARGV.inspect}"
puts "Options: #{options.inspect}"

Validation logic for parsed options should occur after successful parsing. OptionParser handles syntactic validation (correct option format, type conversion) but not semantic validation (valid ranges, file existence, etc.).

options, files = parse_options(ARGV)

# Semantic validation after parsing
if options[:workers] && options[:workers] < 1
  $stderr.puts "Worker count must be positive"
  exit 1
end

if options[:format] == 'xml' && !options[:stylesheet]
  $stderr.puts "XML format requires stylesheet option"
  exit 1
end

Production Patterns

Production command-line applications require robust option parsing that handles edge cases, provides clear feedback, and integrates with logging and configuration systems. OptionParser patterns for production code emphasize error handling, user experience, and maintainability.

Configuration precedence systems use OptionParser alongside configuration files and environment variables. Command-line options typically override configuration file settings, which override default values. This pattern provides flexibility while maintaining predictable behavior.

class AppConfig
  attr_accessor :host, :port, :debug, :log_level
  
  def initialize
    @host = ENV['APP_HOST'] || 'localhost'
    @port = ENV['APP_PORT']&.to_i || 3000
    @debug = false
    @log_level = 'info'
  end
  
  def parse_options(args = ARGV)
    parser = OptionParser.new do |opts|
      opts.banner = "Usage: myapp [options]"
      
      opts.on("-h", "--host HOST", "Server host") { |h| @host = h }
      opts.on("-p", "--port PORT", Integer, "Server port") { |p| @port = p }
      opts.on("-d", "--debug", "Debug mode") { @debug = true }
      opts.on("-l", "--log-level LEVEL", %w[debug info warn error], 
              "Log level") { |l| @log_level = l }
      
      opts.on_tail("--help", "Show help") do
        puts opts
        exit
      end
    end
    
    begin
      parser.parse!(args)
      validate_config
    rescue OptionParser::ParseError => e
      $stderr.puts "Error: #{e}"
      $stderr.puts parser
      exit 1
    end
    
    self
  end
  
  private
  
  def validate_config
    unless (1..65535).include?(@port)
      raise "Port must be between 1 and 65535"
    end
  end
end

Subcommand patterns split complex applications into focused command modules. Each subcommand defines its own option parser while sharing common options through inheritance or composition.

class CLIApp
  def initialize
    @global_options = {}
  end
  
  def run(args = ARGV)
    parser = OptionParser.new do |opts|
      opts.banner = "Usage: myapp [global_options] command [command_options]"
      
      opts.on("-v", "--verbose", "Verbose output") do
        @global_options[:verbose] = true
      end
      
      opts.on("-c", "--config FILE", "Config file") do |file|
        @global_options[:config] = file
      end
    end
    
    # Parse global options, stopping at first non-option
    parser.order!(args)
    
    command = args.shift
    case command
    when 'deploy'
      DeployCommand.new(@global_options).run(args)
    when 'backup'
      BackupCommand.new(@global_options).run(args)
    else
      puts "Unknown command: #{command}"
      puts parser
      exit 1
    end
  end
end

class DeployCommand
  def initialize(global_options)
    @global_options = global_options
    @options = { strategy: 'rolling' }
  end
  
  def run(args)
    parser = OptionParser.new do |opts|
      opts.banner = "Usage: myapp deploy [options] environment"
      
      opts.on("-s", "--strategy STRATEGY", %w[rolling blue-green], 
              "Deployment strategy") { |s| @options[:strategy] = s }
      
      opts.on("-t", "--timeout SECONDS", Integer, 
              "Deployment timeout") { |t| @options[:timeout] = t }
    end
    
    parser.parse!(args)
    
    environment = args.first
    unless environment
      puts "Environment required"
      puts parser
      exit 1
    end
    
    deploy(environment)
  end
  
  private
  
  def deploy(environment)
    puts "Deploying to #{environment} using #{@options[:strategy]} strategy"
    puts "Global config: #{@global_options[:config]}" if @global_options[:config]
  end
end

Logging integration connects command-line options with application logging systems. Production applications often allow log level and destination configuration through command-line options.

require 'logger'

class LoggedApp
  def initialize
    @options = {}
    setup_option_parser
  end
  
  def run(args = ARGV)
    @parser.parse!(args)
    @logger = create_logger
    
    @logger.info "Application starting with options: #{@options}"
    
    begin
      execute_main_logic
    rescue => e
      @logger.error "Application error: #{e}"
      @logger.debug e.backtrace.join("\n") if @options[:debug]
      exit 1
    end
  end
  
  private
  
  def setup_option_parser
    @parser = OptionParser.new do |opts|
      opts.banner = "Usage: logapp [options]"
      
      opts.on("--log-file FILE", "Log to file") { |f| @options[:log_file] = f }
      opts.on("--log-level LEVEL", %w[DEBUG INFO WARN ERROR FATAL],
              "Log level") { |l| @options[:log_level] = l }
      opts.on("--debug", "Debug mode") { @options[:debug] = true }
      opts.on("-q", "--quiet", "Quiet mode") { @options[:quiet] = true }
    end
  end
  
  def create_logger
    output = @options[:log_file] ? File.open(@options[:log_file], 'a') : $stdout
    logger = Logger.new(output)
    
    level = if @options[:quiet]
              Logger::ERROR
            elsif @options[:debug]
              Logger::DEBUG
            else
              Logger.const_get(@options[:log_level] || 'INFO')
            end
    
    logger.level = level
    logger.formatter = proc do |severity, datetime, progname, msg|
      "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
    end
    
    logger
  end
  
  def execute_main_logic
    @logger.info "Executing main application logic"
    # Application logic here
  end
end

Common Pitfalls

OptionParser behavior includes several edge cases and gotchas that can cause unexpected results in command-line parsing. Understanding these pitfalls prevents common mistakes and improves application reliability.

Option ordering affects parsing results when using #parse! versus #parse. The destructive #parse! method modifies ARGV in place, which can cause issues if the application expects to access original arguments after parsing.

original_args = ["-v", "--port", "8080", "input.txt"]
args_copy = original_args.dup

parser = OptionParser.new do |opts|
  opts.on("-v", "--verbose") { }
  opts.on("--port PORT", Integer) { }
end

parser.parse!(args_copy)
puts "Original: #{original_args}"     # ["-v", "--port", "8080", "input.txt"]
puts "After parse!: #{args_copy}"     # ["input.txt"]

# Use parse instead to preserve original
result = parser.parse(original_args)
puts "Non-destructive: #{result}"     # ["input.txt"]

Argument separation issues occur when option arguments contain spaces or special characters. Users must quote arguments containing spaces, but applications may need to handle both quoted and unquoted inputs appropriately.

# Problematic: spaces in arguments
# Command line: myapp --message Hello World --file data.txt
# Parsed as: message="Hello", and "World" becomes a non-option argument

parser = OptionParser.new do |opts|
  opts.on("-m", "--message MSG", "Message text") { |msg| puts "Message: #{msg}" }
  opts.on("-f", "--file FILE", "Input file") { |file| puts "File: #{file}" }
end

# Correct usage requires quoting
# myapp --message "Hello World" --file data.txt
# Or: myapp --message="Hello World" --file=data.txt

Boolean option handling can be counterintuitive when dealing with default values and negation. OptionParser does not automatically provide negation options, and false defaults require explicit handling.

options = { verbose: false, debug: true }  # Note different defaults

parser = OptionParser.new do |opts|
  opts.on("-v", "--verbose", "Enable verbose mode") { options[:verbose] = true }
  opts.on("-q", "--quiet", "Disable verbose mode") { options[:verbose] = false }
  
  # No automatic --no-debug option created
  opts.on("-d", "--debug", "Enable debug mode") { options[:debug] = true }
  opts.on("--no-debug", "Disable debug mode") { options[:debug] = false }
end

# Users might expect --no-verbose to work, but it doesn't
begin
  parser.parse!(["--no-verbose"])
rescue OptionParser::InvalidOption => e
  puts "Error: #{e}"  # "invalid option: --no-verbose"
end

Type conversion edge cases cause parsing failures when arguments don't match expected formats. OptionParser's automatic conversion can be too strict or too lenient depending on the use case.

parser = OptionParser.new do |opts|
  opts.on("-p", "--port PORT", Integer) { |p| puts "Port: #{p}" }
  opts.on("-r", "--rate RATE", Float) { |r| puts "Rate: #{r}" }
end

# These work as expected
parser.parse!(["-p", "8080"])        # Port: 8080
parser.parse!(["-r", "1.5"])         # Rate: 1.5

# But these might surprise users
begin
  parser.parse!(["-p", "8080.5"])    # Fails - Float to Integer conversion
rescue OptionParser::InvalidArgument => e
  puts "Error: #{e}"
end

begin
  parser.parse!(["-p", "08080"])     # Succeeds - becomes 4160 (octal!)
rescue OptionParser::InvalidArgument => e
  puts "Port parsed as octal: unexpected result"
end

Array option parsing splits arguments on commas by default, but this behavior might not match user expectations for file paths or other data containing commas.

options = {}
parser = OptionParser.new do |opts|
  opts.on("-f", "--files FILES", Array, "Input files") { |files| options[:files] = files }
end

parser.parse!(["-f", "a.txt,b.txt,c.txt"])
puts options[:files]  # ["a.txt", "b.txt", "c.txt"]

# Problematic with file paths containing commas
parser.parse!(["-f", "My Document, version 1.txt,other.txt"])
puts options[:files]  # ["My Document", " version 1.txt", "other.txt"]

Option precedence and multiple definitions can create confusion when the same option appears multiple times. OptionParser executes the block for each occurrence, which may not match expected behavior.

count = 0
parser = OptionParser.new do |opts|
  opts.on("-v", "--verbose", "Increase verbosity") { count += 1 }
end

parser.parse!(["-v", "-v", "--verbose"])
puts "Verbosity level: #{count}"  # 3

# Alternative approach for accumulating options
verbosity = 0
parser = OptionParser.new do |opts|
  opts.on("-v", "--verbose", "Increase verbosity") { verbosity += 1 }
  opts.on("-q", "--quiet", "Decrease verbosity") { verbosity -= 1 }
end

Error message clarity suffers when option descriptions are unclear or when the help text doesn't match actual behavior. Production applications should provide clear, actionable error messages.

# Poor error handling
parser = OptionParser.new do |opts|
  opts.on("-x") { }  # No description, confusing help
end

begin
  parser.parse!(["--unknown"])
rescue OptionParser::InvalidOption
  puts parser.help  # Shows -x with no explanation
end

# Better error handling with context
parser = OptionParser.new do |opts|
  opts.banner = "Usage: myapp [options] files..."
  opts.on("-x", "--extract", "Extract files from archive") { }
  
  opts.on_tail("-h", "--help", "Show this help message") do
    puts opts
    puts "\nExamples:"
    puts "  myapp -x archive.tar files/"
    puts "  myapp --extract --verbose archive.zip"
    exit
  end
end

Reference

Core Methods

Method Parameters Returns Description
OptionParser.new(&block) block (optional) OptionParser Creates new parser, yields to configuration block
#on(*args, &block) args (option patterns), block self Defines option with processing block
#parse(argv) argv (Array) Array Parses arguments, returns remaining args
#parse!(argv) argv (Array) Array Destructively parses arguments
#order(argv) argv (Array) Array Parses options in order, stops at first non-option
#order!(argv) argv (Array) Array Destructively parses options in order
#permute(argv) argv (Array) Array Parses options throughout argument list
#permute!(argv) argv (Array) Array Destructively parses options throughout list

Option Definition Patterns

Pattern Example Description
Short flag -v Single dash, single character, no argument
Long flag --verbose Double dash, word, no argument
Short with argument -f FILE Short option requiring argument
Long with argument --file FILE Long option requiring argument
Optional argument -p [PORT] Argument in brackets, optional
Combined formats -f, --file FILE Short and long variants

Type Conversion Classes

Type Example Usage Conversion Result
Integer opts.on("-p PORT", Integer) Converts string to integer
Float opts.on("-r RATE", Float) Converts string to float
Numeric opts.on("-n NUM", Numeric) Converts to Integer or Float
TrueClass opts.on("-v", TrueClass) Always returns true
FalseClass opts.on("-q", FalseClass) Always returns false
Array opts.on("-l LIST", Array) Splits on commas
Regexp opts.on("-m REGEX", Regexp) Compiles string to regexp

Exception Hierarchy

Exception Cause Recovery Strategy
OptionParser::ParseError Base class for parsing errors Catch-all for option errors
OptionParser::InvalidOption Unknown option encountered Show help, suggest similar options
OptionParser::InvalidArgument Argument type conversion failed Validate input format, show examples
OptionParser::MissingArgument Required argument missing Prompt for argument, show usage
OptionParser::AmbiguousOption Option matches multiple definitions List matching options

Banner and Help Methods

Method Parameters Returns Description
#banner= string String Sets usage line in help text
#program_name= string String Sets program name for help
#version= string String Sets version for --version option
#ver none String Returns version string
#help none String Returns formatted help text
#to_s none String Alias for help method
#on_tail(*args, &block) args, block self Adds option at end of help
#on_head(*args, &block) args, block self Adds option at start of help

Parsing Behavior Options

Method Effect Use Case
#parse Non-destructive parsing When original ARGV needed
#parse! Modifies input array Standard CLI processing
#order Stops at first non-option Subcommand parsing
#permute Processes options throughout Mixed options and arguments

Common Option Patterns

# Help and version options
opts.on_tail("-h", "--help", "Show help") { puts opts; exit }
opts.on_tail("--version", "Show version") { puts opts.ver; exit }

# Verbose and quiet modes
opts.on("-v", "--verbose", "Verbose output") { options[:verbose] = true }
opts.on("-q", "--quiet", "Minimal output") { options[:quiet] = true }

# Configuration file
opts.on("-c", "--config FILE", "Config file path") { |f| options[:config] = f }

# Numeric options with validation
opts.on("-p", "--port PORT", Integer, "Port (1-65535)") do |p|
  unless (1..65535).include?(p)
    raise OptionParser::InvalidArgument, "Port must be 1-65535"
  end
  options[:port] = p
end

# Multiple choice options
opts.on("-m", "--mode MODE", %w[fast normal secure], "Processing mode") do |m|
  options[:mode] = m
end