CrackedRuby logo

CrackedRuby

GetoptLong

Overview

GetoptLong provides command-line option parsing similar to GNU getopt_long(), handling both short options like -v and long options like --verbose. Ruby implements GetoptLong as a class that processes ARGV arguments according to defined option specifications, supporting required arguments, optional arguments, and boolean flags.

The parser operates by consuming options from ARGV in order, returning each parsed option as a name-value pair. GetoptLong supports POSIX-style option clustering (combining multiple short options like -abc) and GNU-style long options with equals assignment (--output=file.txt).

require 'getoptlong'

opts = GetoptLong.new(
  ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
  ['--file', '-f', GetoptLong::REQUIRED_ARGUMENT]
)

opts.each do |opt, arg|
  case opt
  when '--verbose'
    puts "Verbose mode enabled"
  when '--file'
    puts "Processing file: #{arg}"
  end
end

GetoptLong modifies ARGV by removing processed options, leaving remaining arguments for application use. The parser stops at the first non-option argument or when encountering the end-of-options marker --.

# Command: ruby script.rb --verbose --file data.txt input1 input2
# After parsing: ARGV contains ['input1', 'input2']

GetoptLong defines three argument types: NO_ARGUMENT for boolean flags, REQUIRED_ARGUMENT for options requiring values, and OPTIONAL_ARGUMENT for options with optional values. The parser validates argument requirements and raises exceptions for malformed options.

Basic Usage

GetoptLong requires option specifications defining the option names, aliases, and argument types. Each specification contains the long option name, optional short alias, and argument requirement constant.

require 'getoptlong'

opts = GetoptLong.new(
  ['--help', '-h', GetoptLong::NO_ARGUMENT],
  ['--output', '-o', GetoptLong::REQUIRED_ARGUMENT],
  ['--count', '-c', GetoptLong::OPTIONAL_ARGUMENT],
  ['--debug', GetoptLong::NO_ARGUMENT]
)

The each method yields option-argument pairs for processing. The parser returns the canonical long option name regardless of which alias the user provided.

verbose = false
output_file = nil
debug_level = 0

opts.each do |option, argument|
  case option
  when '--help'
    puts "Usage: #{$0} [options] files..."
    exit 0
  when '--output'
    output_file = argument
  when '--count'
    count = argument.empty? ? 10 : argument.to_i
  when '--debug'
    debug_level += 1
  end
end

puts "Remaining arguments: #{ARGV.join(', ')}" unless ARGV.empty?

GetoptLong supports option ordering modes that control how options and non-option arguments intermix. The default PERMUTE mode moves options before non-option arguments, while REQUIRE_ORDER stops parsing at the first non-option.

opts = GetoptLong.new(
  ['--verbose', '-v', GetoptLong::NO_ARGUMENT]
)
opts.ordering = GetoptLong::REQUIRE_ORDER

# Command: ruby script.rb file1 --verbose file2
# Only processes file1, stops before --verbose

Multiple instances of the same option accumulate through repeated iteration. Applications typically use counters or arrays to handle repeated options.

verbose_level = 0
include_paths = []

opts.each do |option, argument|
  case option
  when '--verbose'
    verbose_level += 1  # -vvv increases level to 3
  when '--include'
    include_paths << argument
  end
end

The parser recognizes several option formats: short options (-v), clustered short options (-abc), long options (--verbose), and long options with arguments (--file=name or --file name). GetoptLong handles argument separation automatically.

# These are equivalent:
# --output=report.txt
# --output report.txt
# -o report.txt
# -oreport.txt

opts = GetoptLong.new(
  ['--output', '-o', GetoptLong::REQUIRED_ARGUMENT]
)

opts.each do |opt, arg|
  puts "Output file: #{arg}"  # Always receives 'report.txt'
end

Error Handling & Debugging

GetoptLong raises GetoptLong::InvalidOption for unrecognized options and GetoptLong::MissingArgument for options missing required arguments. Applications should wrap parsing in exception handlers to provide user-friendly error messages.

require 'getoptlong'

opts = GetoptLong.new(
  ['--file', '-f', GetoptLong::REQUIRED_ARGUMENT],
  ['--count', '-c', GetoptLong::REQUIRED_ARGUMENT]
)

begin
  opts.each do |option, argument|
    case option
    when '--file'
      unless File.exist?(argument)
        STDERR.puts "Error: File '#{argument}' not found"
        exit 1
      end
      @input_file = argument
    when '--count'
      @count = Integer(argument)
    end
  end
rescue GetoptLong::InvalidOption => e
  STDERR.puts "Unknown option: #{e.message}"
  STDERR.puts "Use --help for usage information"
  exit 1
rescue GetoptLong::MissingArgument => e
  STDERR.puts "Option requires an argument: #{e.message}"
  exit 1
rescue ArgumentError => e
  STDERR.puts "Invalid argument format: #{e.message}"
  exit 1
end

The quiet attribute controls whether GetoptLong prints error messages to STDERR. Setting quiet to true suppresses automatic error output, giving applications full control over error presentation.

opts = GetoptLong.new(
  ['--verbose', '-v', GetoptLong::NO_ARGUMENT]
)
opts.quiet = true

begin
  opts.each { |opt, arg| puts "Got #{opt}" }
rescue GetoptLong::InvalidOption => e
  puts "Custom error: Unknown option #{e.message}"
end

Debugging option parsing requires understanding how GetoptLong processes ARGV. The get method returns individual option-argument pairs, allowing step-by-step parsing inspection.

opts = GetoptLong.new(
  ['--debug', GetoptLong::NO_ARGUMENT],
  ['--file', GetoptLong::REQUIRED_ARGUMENT]
)

loop do
  begin
    option, argument = opts.get
    break if option.nil?
    puts "Parsed: #{option} = #{argument.inspect}"
  rescue GetoptLong::InvalidOption => e
    puts "Invalid option encountered: #{e}"
    break
  end
end

puts "Remaining ARGV: #{ARGV.inspect}"

Validation beyond GetoptLong's built-in checks requires custom argument processing. Applications should validate file paths, numeric ranges, and enum values after option parsing completes.

def validate_options(options)
  errors = []
  
  if options[:count] && options[:count] <= 0
    errors << "Count must be positive"
  end
  
  if options[:format] && !%w[json xml csv].include?(options[:format])
    errors << "Format must be json, xml, or csv"
  end
  
  if options[:input] && !File.readable?(options[:input])
    errors << "Cannot read input file: #{options[:input]}"
  end
  
  unless errors.empty?
    STDERR.puts "Validation errors:"
    errors.each { |error| STDERR.puts "  #{error}" }
    exit 1
  end
end

Common Pitfalls

Option specifications must include the long option name first, followed by optional short aliases, then the argument type. Reversing this order or omitting the argument type causes runtime errors.

# Wrong - argument type missing
opts = GetoptLong.new(['--verbose', '-v'])

# Wrong - short option first  
opts = GetoptLong.new(['-v', '--verbose', GetoptLong::NO_ARGUMENT])

# Correct
opts = GetoptLong.new(['--verbose', '-v', GetoptLong::NO_ARGUMENT])

GetoptLong modifies ARGV during parsing, which can cause issues when multiple parsers process the same arguments or when applications need the original arguments. Save ARGV before parsing if the original arguments are needed.

original_args = ARGV.dup

opts = GetoptLong.new(['--verbose', GetoptLong::NO_ARGUMENT])
opts.each { |opt, arg| puts "Processing #{opt}" }

puts "Original: #{original_args.join(' ')}"
puts "Modified: #{ARGV.join(' ')}"

Clustered short options like -abc expand to -a -b -c, but only if all options are NO_ARGUMENT type. Mixing argument-requiring options in clusters produces unexpected behavior.

opts = GetoptLong.new(
  ['--all', '-a', GetoptLong::NO_ARGUMENT],
  ['--file', '-f', GetoptLong::REQUIRED_ARGUMENT],
  ['--count', '-c', GetoptLong::REQUIRED_ARGUMENT]
)

# Command: ruby script.rb -af output.txt
# GetoptLong interprets this as: -a -f output.txt
# NOT as: -a -f=output.txt or -af=output.txt

# Safe clustering (all NO_ARGUMENT):
# ruby script.rb -abc
# Expands to: -a -b -c

The OPTIONAL_ARGUMENT type behaves differently between short and long options. Long options require equals syntax for optional arguments, while short options consume the next argument if it doesn't start with a dash.

opts = GetoptLong.new(
  ['--level', '-l', GetoptLong::OPTIONAL_ARGUMENT]
)

# These work correctly:
# --level=5    (argument is "5")
# --level      (argument is "")
# -l5          (argument is "5") 
# -l           (argument is "" if next arg starts with -)

# This might not work as expected:
# -l 5         (argument might be "5" or "" depending on context)

GetoptLong stops parsing at the first unrecognized argument when ordering is REQUIRE_ORDER, but continues parsing remaining options in PERMUTE mode. This can lead to inconsistent behavior across different option orderings.

opts = GetoptLong.new(['--verbose', GetoptLong::NO_ARGUMENT])
opts.ordering = GetoptLong::REQUIRE_ORDER

# Command: ruby script.rb input.txt --verbose
# Result: ARGV = ['input.txt', '--verbose'] (--verbose not parsed)

opts.ordering = GetoptLong::PERMUTE  # Default
# Command: ruby script.rb input.txt --verbose  
# Result: ARGV = ['input.txt'] (--verbose parsed and removed)

Exception handling must account for GetoptLong's parsing state. Once an exception occurs, the parser becomes unusable and subsequent calls to each or get may produce inconsistent results.

opts = GetoptLong.new(['--count', GetoptLong::REQUIRED_ARGUMENT])

begin
  opts.each do |opt, arg|
    puts "Processing #{opt}: #{arg}"
  end
rescue GetoptLong::MissingArgument
  puts "Error occurred - parser state is undefined"
  # Don't continue using opts after exception
end

Production Patterns

Command-line applications benefit from structured option handling that separates parsing from business logic. Create a configuration class that encapsulates option processing and validation.

require 'getoptlong'

class ApplicationConfig
  attr_reader :verbose, :input_files, :output_format, :max_threads
  
  def initialize(argv = ARGV)
    @verbose = false
    @input_files = []
    @output_format = 'text'
    @max_threads = 1
    @help_requested = false
    
    parse_options
    validate_configuration
    @input_files = argv.dup  # Remaining arguments after option parsing
  end
  
  def show_help?
    @help_requested
  end
  
  private
  
  def parse_options
    opts = GetoptLong.new(
      ['--help', '-h', GetoptLong::NO_ARGUMENT],
      ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
      ['--format', '-f', GetoptLong::REQUIRED_ARGUMENT],
      ['--threads', '-t', GetoptLong::REQUIRED_ARGUMENT],
      ['--config', '-c', GetoptLong::REQUIRED_ARGUMENT]
    )
    
    opts.quiet = true
    
    opts.each do |option, argument|
      case option
      when '--help'
        @help_requested = true
      when '--verbose'
        @verbose = true
      when '--format'
        @output_format = argument.downcase
      when '--threads'
        @max_threads = Integer(argument)
      when '--config'
        load_config_file(argument)
      end
    end
  rescue GetoptLong::InvalidOption, GetoptLong::MissingArgument, ArgumentError => e
    STDERR.puts "Option error: #{e.message}"
    STDERR.puts "Use --help for usage information"
    exit 1
  end
  
  def validate_configuration
    unless %w[text json xml csv].include?(@output_format)
      raise "Invalid output format: #{@output_format}"
    end
    
    if @max_threads < 1 || @max_threads > 32
      raise "Thread count must be between 1 and 32"
    end
  end
  
  def load_config_file(path)
    # Configuration file processing logic
    unless File.readable?(path)
      raise "Cannot read configuration file: #{path}"
    end
  end
end

# Usage in main application
begin
  config = ApplicationConfig.new
  
  if config.show_help?
    puts "Usage: #{$0} [options] input_files..."
    puts "Options:"
    puts "  -h, --help           Show this help"
    puts "  -v, --verbose        Enable verbose output"
    puts "  -f, --format FORMAT  Output format (text, json, xml, csv)"
    puts "  -t, --threads COUNT  Number of processing threads"
    puts "  -c, --config FILE    Load configuration from file"
    exit 0
  end
  
  processor = DataProcessor.new(config)
  processor.process_files(config.input_files)
  
rescue StandardError => e
  STDERR.puts "Error: #{e.message}"
  exit 1
end

For complex CLI applications, implement subcommand support by parsing the first argument as a command name, then creating command-specific option parsers.

class CLIApplication
  def initialize
    @global_verbose = false
    @command = nil
    @command_args = []
  end
  
  def run
    parse_global_options
    execute_command
  end
  
  private
  
  def parse_global_options
    opts = GetoptLong.new(
      ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
      ['--help', '-h', GetoptLong::NO_ARGUMENT]
    )
    opts.ordering = GetoptLong::REQUIRE_ORDER
    
    opts.each do |option, argument|
      case option
      when '--verbose'
        @global_verbose = true
      when '--help'
        show_global_help
        exit 0
      end
    end
    
    if ARGV.empty?
      STDERR.puts "No command specified"
      show_global_help
      exit 1
    end
    
    @command = ARGV.shift
    @command_args = ARGV.dup
  end
  
  def execute_command
    case @command
    when 'process'
      ProcessCommand.new(@command_args, @global_verbose).execute
    when 'analyze'
      AnalyzeCommand.new(@command_args, @global_verbose).execute
    when 'report'
      ReportCommand.new(@command_args, @global_verbose).execute
    else
      STDERR.puts "Unknown command: #{@command}"
      exit 1
    end
  end
  
  def show_global_help
    puts "Usage: #{$0} [global-options] command [command-options]"
    puts "Global options:"
    puts "  -v, --verbose    Enable verbose output"
    puts "  -h, --help       Show this help"
    puts ""
    puts "Commands:"
    puts "  process          Process input files"
    puts "  analyze          Analyze data patterns"
    puts "  report           Generate reports"
  end
end

class ProcessCommand
  def initialize(args, verbose)
    @args = args
    @global_verbose = verbose
    @input_files = []
    @output_dir = '.'
    @parallel = false
  end
  
  def execute
    parse_options
    
    if @input_files.empty?
      STDERR.puts "No input files specified"
      exit 1
    end
    
    puts "Processing #{@input_files.length} files..." if @global_verbose
    # Processing logic here
  end
  
  private
  
  def parse_options
    ARGV.replace(@args)  # Reset ARGV for this command's parsing
    
    opts = GetoptLong.new(
      ['--input', '-i', GetoptLong::REQUIRED_ARGUMENT],
      ['--output', '-o', GetoptLong::REQUIRED_ARGUMENT],
      ['--parallel', '-p', GetoptLong::NO_ARGUMENT]
    )
    
    opts.each do |option, argument|
      case option
      when '--input'
        @input_files << argument
      when '--output'
        @output_dir = argument
      when '--parallel'
        @parallel = true
      end
    end
    
    @input_files.concat(ARGV)  # Add remaining arguments as input files
  end
end

Production applications should implement comprehensive logging that captures both option parsing decisions and application behavior. This facilitates troubleshooting deployment issues and user error reports.

require 'logger'

class ProductionCLI
  def initialize
    @logger = Logger.new(STDERR)
    @logger.level = Logger::WARN
    @options = {}
  end
  
  def run
    parse_options
    setup_logging
    execute_application
  end
  
  private
  
  def parse_options
    opts = GetoptLong.new(
      ['--log-level', GetoptLong::REQUIRED_ARGUMENT],
      ['--log-file', GetoptLong::REQUIRED_ARGUMENT],
      ['--dry-run', GetoptLong::NO_ARGUMENT],
      ['--config', GetoptLong::REQUIRED_ARGUMENT]
    )
    
    opts.each do |option, argument|
      @logger.debug "Parsing option: #{option} = #{argument}"
      
      case option
      when '--log-level'
        @options[:log_level] = argument.upcase
      when '--log-file'
        @options[:log_file] = argument
      when '--dry-run'
        @options[:dry_run] = true
      when '--config'
        @options[:config_file] = argument
      end
    end
    
    @logger.info "Option parsing completed: #{@options.inspect}"
  end
  
  def setup_logging
    if @options[:log_level]
      @logger.level = Logger.const_get(@options[:log_level])
    end
    
    if @options[:log_file]
      @logger = Logger.new(@options[:log_file])
      @logger.level = Logger.const_get(@options[:log_level] || 'INFO')
    end
    
    @logger.info "Application started with options: #{@options.inspect}"
    @logger.info "Remaining arguments: #{ARGV.inspect}"
  end
  
  def execute_application
    @logger.info "Beginning application execution"
    
    if @options[:dry_run]
      @logger.info "Dry run mode - no changes will be made"
    end
    
    # Application logic with comprehensive logging
    @logger.info "Application execution completed successfully"
  rescue StandardError => e
    @logger.error "Application failed: #{e.message}"
    @logger.error e.backtrace.join("\n")
    exit 1
  end
end

Reference

GetoptLong Class Methods

Method Parameters Returns Description
new(*arguments) *arguments (Array) GetoptLong Creates parser with option specifications

GetoptLong Instance Methods

Method Parameters Returns Description
each &block GetoptLong Iterates over parsed options yielding name, argument
get None Array Returns next option as [name, argument] or [nil, nil]
ordering=(mode) mode (Integer) Integer Sets option ordering mode
ordering None Integer Returns current ordering mode
quiet=(boolean) boolean (Boolean) Boolean Sets error message suppression
quiet None Boolean Returns error suppression setting
terminate None GetoptLong Terminates option processing
terminated? None Boolean Returns true if processing terminated

Argument Type Constants

Constant Value Description
NO_ARGUMENT 0 Option takes no argument (boolean flag)
REQUIRED_ARGUMENT 1 Option requires an argument
OPTIONAL_ARGUMENT 2 Option argument is optional

Ordering Mode Constants

Constant Value Description
PERMUTE 0 Permute arguments (default) - options processed anywhere
REQUIRE_ORDER 1 Stop processing at first non-option argument
RETURN_IN_ORDER 2 Return non-options as arguments to option ''

Exception Classes

Exception Inheritance Description
GetoptLong::Error StandardError Base class for GetoptLong exceptions
GetoptLong::AmbiguousOption GetoptLong::Error Ambiguous option abbreviation
GetoptLong::InvalidOption GetoptLong::Error Unrecognized option
GetoptLong::MissingArgument GetoptLong::Error Required argument missing
GetoptLong::NeedlessArgument GetoptLong::Error Argument provided to NO_ARGUMENT option

Option Specification Format

# Format: [long_option, short_option, argument_type]
['--verbose', '-v', GetoptLong::NO_ARGUMENT]
['--file', '-f', GetoptLong::REQUIRED_ARGUMENT]
['--count', GetoptLong::OPTIONAL_ARGUMENT]  # No short option

Supported Option Formats

Format Example Argument Type Description
-v -v NO_ARGUMENT Short boolean flag
-f arg -f file.txt REQUIRED_ARGUMENT Short option with separate argument
-farg -ffile.txt REQUIRED_ARGUMENT Short option with attached argument
--flag --verbose NO_ARGUMENT Long boolean flag
--opt arg --file data.txt REQUIRED_ARGUMENT Long option with separate argument
--opt=arg --file=data.txt REQUIRED_ARGUMENT Long option with equals argument
-abc -abc All NO_ARGUMENT Clustered short options

Common Usage Patterns

# Basic option parsing
opts = GetoptLong.new(
  ['--help', '-h', GetoptLong::NO_ARGUMENT],
  ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
  ['--file', '-f', GetoptLong::REQUIRED_ARGUMENT]
)

# Error handling pattern  
begin
  opts.each do |opt, arg|
    # Process options
  end
rescue GetoptLong::InvalidOption, GetoptLong::MissingArgument => e
  STDERR.puts "Error: #{e.message}"
  exit 1
end

# Accumulating repeated options
count = 0
opts.each do |opt, arg|
  case opt
  when '--verbose'
    count += 1
  end
end