Ruby OptionParser

Vinicius Negrisolo Vinicius Negrisolo Ruby

In order to parse user inputs passed to CLI Scripts we can use the Ruby class OptionParser. With that it’s possible to define an argument specification like required/optional, or restrict values or even convert into a specified class. Find out how to configure OptionParser and use it.

User Inputs

It’s quite common for CLI Scripts to receive arguments like:

flag and value description
-f short style without any value
-f bar value separated by whitespace
-f=bar value separated by =
--foo bar long style with value
--foo bar,baz multiple values (list)

This is so common that this become implemented by OptionParser class. Let’s see how to configure all that.

OptionParser class

The Ruby standard library has a class called OptionParser that handle user inputs in a very easy way. The main idea is to define a parser and parse it, simple as:

OptionParser.new do |parser|
  # define your parser here
end.parse!

The parse! method has a default of ARGV and so you can receive ruby script arguments.

Another nice catch is to use a Struct class to hold the configuration variables parsed by OptionParser, and possibly with default values, something like this:

Config = Struct.new(*%i[color drink lang fun point sports time user]) do
  def initialize
    self.fun    = false
    self.point  = 0
    self.sports = []
  end
end

Finally it’s important to memoize the Config instance, so we are going to have just one parser and call parse! method just once.

class OptparseExample
  class << self
    def config
      @config ||= Config.new.tap do |config|
        build_parser(config).parse!
      end
    end
  end
end

Complete example

Maybe the easiest way to explain how to configure and use OptionParser is providing a full example with a Ruby script file and call it with some arguments. Check this out:

Config = Struct.new(*%i[color drink lang fun point sports time user]) do
  DRINKS = %i[water tea beer]
  LANGS  = {
    de: 'Deutsch',
    en: 'English',
    fr: 'Français',
  }

  def self.drinks
    DRINKS
  end

  def self.langs
    LANGS
  end

  def initialize
    self.drink  = DRINKS.first
    self.lang   = LANGS.values.first
    self.fun    = false
    self.point  = 0
    self.sports = []
  end
end

The User class just represents a simple model, and this class is being used for Custom Converters. In this case I want to receive a user by id, fetch him in the database and provide as a configuration the found User, not just the id. This is just to show how powerful can be a Custom Converter.

User = Struct.new(:name) do
  def self.find(id)
    all[id - 1]
  end

  def self.all
    [
      User.new('John'),
      User.new('Mary'),
    ]
  end
end

Finally the full Ruby script file:

#!/usr/bin/env ruby

require 'optparse'
require 'optparse/time'

class OptparseExample
  class << self
    def config
      @config ||= Config.new.tap do |config|
        build_parser(config).parse!
      end
    end

    private

    def build_parser(config)
      OptionParser.new do |parser|
        parser.banner = 'Help: ./my-script --help'
        define_custom_converters(parser)

        parser.separator "\nCommon options:"
        define_common_config(parser, config)

        parser.separator "\nSpecific options:"
        define_specific_config(parser, config)
      end
    end

    def define_custom_converters(parser)
      parser.accept(User) { |id| User.find(id.to_i) }
    end

    def define_common_config(parser, config)
      parser.on('-h', '--help',    'Show help')    { quit_script(parser) }
      parser.on('-v', '--version', 'Show version') { quit_script('Version => 1.0.0') }
    end

    def define_specific_config(parser, config)
      parser.on('-c', '--color=COLOR', 'Favorite Color')           { |v| config.color  = v }
      parser.on('-d', '--drink=DRINK', drinks, "Drink: #{drinks}") { |v| config.drink  = v }
      parser.on('-l', '--lang=LANG', langs, "Lang: #{langs}")      { |v| config.lang   = v }
      parser.on('-f', '--[no-]fun', 'Run with Fun Mode')           { |v| config.fun    = v }
      parser.on('-p', '--point=POINT', Float, 'Points')            { |v| config.point  = v }
      parser.on('-s', '--sports=X,Y,Z', Array, 'Favorite Sports')  { |v| config.sports = v }
      parser.on('-t', '--time=TIME', Time, 'Starting time')        { |v| config.time   = v }
      parser.on('-u', '--user=USER_ID', User, 'User ID')           { |v| config.user   = v }
    end

    def quit_script(message = nil)
      puts message if message
      exit
    end

    def drinks
      Config.drinks
    end

    def langs
      Config.langs
    end
  end
end

puts OptparseExample.config

If we run this script with the --help flag:

./my-ruby-script --help
#=> Usage: ./my-script [options]
#=>
#=> Common options:
#=>     -v, --version                    Show version
#=>     -h, --help                       Show help
#=>
#=> Specific options:
#=>     -c, --color=COLOR                Favorite Color
#=>     -d, --drink=DRINK                Drink: [:water, :tea, :beer]
#=>     -l, --lang=LANG                  Lang: {:de=>"Deutsch", :en=>"English", :fr=>"Français"}
#=>     -f, --[no-]fun                   Run with Fun Mode
#=>     -p, --point=POINT                Points
#=>     -s, --sports=X,Y,Z               Favorite Sports
#=>     -t, --time=TIME                  Starting time
#=>     -u, --user=USER_ID               User ID

Now running the same script setting a lot of different flags:

./my-ruby-script -c blue \
                 -d tea \
                 -l en \
                 -f \
                 -p 5 \
                 -s hockey,soccer,rugby \
                 -t 14/07/2016-08:53:20 \
                 -u 1;

#=> #<struct Config
#=>   color="blue",
#=>   drink=:tea,
#=>   lang="English",
#=>   fun=true,
#=>   point=5.0,
#=>   sports=["hockey", "soccer", "rugby"],
#=>   time=2016-07-14 08:53:20 -0300,
#=>   user=#<struct User name="John">
#=> >

Try yourself with this code and verify what happens if you change the flags configuration or values when calling the script.

Conclusion

OptionParser class is a simplifier class when we are dealing with Ruby Script files. It helps us to define and parse an user input and with this definition we can use it to print a help message in the same script. Let’s use it to get user inputs, restrict to a list of possible values, make them required, convert to specific classes and so on.


Read also:

Struct Factory for Elixir Elixir

How easy is to build a Factory solution for Elixir applications? In this post I share a simple 20ish lines-of-code solution and its testing ✅. Check this out and start using factories for building data for tests and seed.

Ruby CLI Script Ruby

To create CLI script that runs Ruby code is super fast and straightforward. With that you can run any Ruby code directly from terminal, including your own gem. Let’s take a look what’s need to be done in order to get a nice maintainable CLI script written in Ruby.