Join my Laravel for REST API's course on Udemy ๐Ÿ‘€

Command-line interfaces with Ruby OptionParser

Updated Oct 16, 2020 ย โ€ย 5ย min read

The Ruby standard library ships with the OptionParser class for parsing command-line options. Besides just parsing options it can take care of help messages and usage information too. This makes building command-line interfaces a breeze compared to using the ARGV constant.

To access the OptionParser class you need to require optparse at the top of you Ruby file. Don't confuse it with a gem. It comes with Ruby, there is no need to install it separately.

require 'optparse'

You get started by creating an instance of the OptionParser class, pass it a block and call parse! on it.

require 'optparse'

OptionParser.new do
  puts 'I found a goldfish'
end.parse!

#=> $ ruby fish_finder.rb
#=> I found a goldfish

Okay, that was pretty useless, a single line of puts would have had the same result. Lets start with adding an option to this fish finder CLI. We can store the option values in a hash by passing in into the parse method.

require 'optparse'

params = {}

OptionParser.new do |parser|
  parser.on('-w', '--water TYPE')
  parser.on('-h', '--help')
  parser.on('-v', '--verbose')
end.parse!(into: params)

puts params

#=> $ ruby fish_finder.rb -v --water salt
#=> {:verbose=>true, :water=>"salt"}

You can note a couple things from the above example already:

  • You can require options to have an argument by adding a argument name to the option specification. See the --water TYPE specification. If you want an optional argument you would use --water [TYPE] in this case.
  • Options you don't pass to the script are not present in the params hash. Note the absence of :help.
  • The long option name is used as a symbol in the params hash.

Now before we continue lets wrap this in a fancy FishFinder class.

require 'optparse'

class FishFinder
  def fish
    'I found a goldfish'
  end

  def parse
    OptionParser.new do |parser|
      parser.on('--water TYPE')
      parser.on('-h', '--help')
      parser.on('-v', '--verbose')
    end.parse!
  end
end

finder = FishFinder.new
finder.parse
puts finder.fish

The OptionParser class provides methods out of the box to help you create usage information messages. We do this by setting a banner for the parser, which is the first line in the usage output. With the seperator method you can add extra lines of text, or a blank line in this case. In the block that is passed for the --help option you see that puts parser is enough to print the usage information. We exit here as well so we don't search for a goldfish while we only want to see the usage information.

require 'optparse'

class FishFinder
  def fish
    'I found a goldfish'
  end

  def parse
    OptionParser.new do |parser|
      parser.banner = 'Usage: fish_finder.rb [options]'
      parser.separator ''

      parser.on('--water TYPE', 'Pick "fresh" or "salt"')

      parser.on('-h', '--help', 'Show this message') do
        puts parser
        exit
      end

      parser.on('-v', '--verbose', 'Show fancy fish')
    end.parse!
  end
end

finder = FishFinder.new
finder.parse
puts finder.fish

#=> $ ruby fish_finder.rb --help
#=> Usage: fish_finder.rb [options]
#=>     --water TYPE                 Pick "fresh" or "salt"
#=> -h, --help                       Show this message
#=> -v, --verbose                    Show extra fish

Boolean switch options

We can use the --verbose flag to overwrite a default configuration. This way we can change the behavior of this fish finder by passing an option. If the --verbose of -v option is passed to the script we reward you with a fancy fish emoji.

require 'optparse'

class FishFinder
  attr_accessor :verbose

  def initialize
    self.verbose = false
  end

  def fish
    fish = self.verbose ? '๐Ÿ ' : 'goldfish'
    "I found a #{fish}"
  end

  def parse
    OptionParser.new do |parser|
      parser.banner = "Usage: fish_finder.rb [options]"
      parser.separator ""

      parser.on('--water TYPE', 'Pick "fresh" or "salt"')

      parser.on('-h', '--help', 'Show this message') do
        puts parser
        exit
      end

      parser.on('-v', '--verbose', 'Show fancy fish') do |verbose|
        self.verbose = verbose
      end
    end.parse!
  end
end

finder = FishFinder.new
finder.parse
puts finder.fish

#=> $ ruby fish_finder.rb
#=> I found a goldfish

#=> $ ruby fish_finder.rb --verbose
#=> I found a ๐Ÿ 

Options with required arguments

By adding an all caps name to the option definition you can define a required argument for an option. This means that an OptionParser::MissingArgument exception is raised when the option is missing. You probably want to catch this and give a nice feedback message.

Now, a second issue might be that you receive an argument that you don't expect. In our case we allow for the water types "fresh" and "salt". We could use the build-in type coercion. So an OptionParser::InvalidArgument exception in thrown when an invalid argument is passed.

parser.on('--water TYPE', String, 'Pick "fresh" or "salt"')

In the above example you see how that would look. We require the TYPE argument to be a non-empty string. You can take a look at the docs for more type coercion options.

Since our argument values are rather specific we can check for a pattern too. See the final example for a check on salt or fresh water.

require 'optparse'

class FishFinder
  attr_accessor :verbose, :filter

  def initialize
    self.verbose = false
  end

  def fish
    fish = self.verbose ? '๐Ÿ ' : self.find_fish
    "I found a #{fish}"
  end

  def find_fish
    if self.filter == 'fresh'
      'goldfish'
    elsif self.filter == 'salt'
      'tuna'
    else
      'sturgeon'
    end
  end

  def parse
    OptionParser.new do |parser|
      parser.banner = "Usage: fish_finder.rb [options]"
      parser.separator ""

      parser.on('--water TYPE', /fresh|salt/, 'Pick "fresh" or "salt"') do |water|
        self.filter = water
      end

      parser.on('-h', '--help', 'Show this message') do
        puts parser
        exit
      end

      parser.on('-v', '--verbose', 'Show extra fish') do |v|
        self.verbose = v
      end
    end.parse!
  end
end

finder = FishFinder.new
finder.parse
puts finder.fish

#=> $ ruby fish_finder.rb --water salt
#=> I found a tuna

#=> $ ruby fish_finder.rb --water fresh
#=> I found a goldfish

#=> $ ruby fish_finder.rb
#=> I found a sturgeon