Mike Slinn

Parsing Command Line Arguments with OptionParser

Published 2023-10-03. Last modified 2024-03-20.
Time to read: 3 minutes.

This page is part of the ruby collection.

The Ruby language has many libraries for parsing command-line arguments. This article discusses OptionParser, which, although only 3 years old when this was written, has become one of the most popular libraries for parsing arguments. This is its GitHub repository.

Installation

OptionParser is part of the standard Ruby runtime library, so once Ruby itself is installed, there are no additional steps for installation.

Gemfile

If you are building an application, add the following line to your application’s Gemfile:

Gemfile
gem 'optparse'

And then execute:

Shell
$ bundle

Gem

If you are building a gem, add the following line to your gem’s .gemspec:

your_gem.gemspec
spec.add_dependency 'optparse'

And then execute:

Shell
$ bundle

Usage

My stabilize_video program is an example of how I like to use OptionParser. Below are portions of three Ruby source files:

  1. option.rb parses the options and generates the help text.
  2. stabilize_video.rb parses the mandatory arguments.
  3. stablize.rb receives the mandatory arguments and options.

Parsing Options

First, let’s look at the parse_options method, which uses OptionParser to parse the optional arguments.

Portion of options.rb
def parse_options
  options = { shake: 5, loglevel: 'warning' }
  OptionParser.new do |parser|
    parser.program_name = File.basename __FILE__
    @parser = parser

    parser.on('-f', '--overwrite', 'Overwrite output file if present')
    parser.on('-l', '--loglevel LOGLEVEL', Integer, "Logging level (#{VERBOSITY.join ', '})")
    parser.on('-s', '--shake SHAKE', Integer, 'Shakiness (1..10)')
    parser.on('-v', '--verbose VERBOSE', 'Zoom percentage')
    parser.on('-z', '--zoom ZOOM', Integer, 'Zoom percentage')

    parser.on_tail('-h', '--help', 'Show this message') do
      help
    end
  end.order!(into: options)
  help "Invalid verbosity value (#{options[:verbose]}), must be one of one of: #{VERBOSITY.join ', '}." if options[:verbose] && !options[:verbose] in VERBOSITY
  help "Invalid shake value (#{options[:shake]})." if options[:shake].negative? || options[:shake] > 10
  options
end
  1. The default options are set in the highlighted hash. Default values are set for the :shake and :loglevel keys.
  2. When parser.on is passed the name of an option value in UPPER CASE, it creates an entry in the options hash with that name, in lower case. The above code shows the following examples:
    • LOGLEVEL provides a means for the user to specify a value to replace the default value of the loglevel entry in the options hash, which was initialized with the string value 'warning'.
    • SHAKE provides a means for the user to specify a to replace the default value of the shake entry in the options hash, which was initialized with the integer value 5.
    • VERBOSE provides a means for the user to specify a string value for a new entry in the options hash, with the key verbose.
    • ZOOM provides a means for the user to specify a string value for a new entry in the options hash, with the key zoom.
  3. OptionParser.order! has the side effect that option keywords and key/value pairs that match parser.on statements are removed from ARGV.
  4. Ending the end.order! statement with (into: options) causes the parsed option key/value pairs to be added or updated in the hash called options.
  5. The parsed options are returned.

Parsing Mandatory Arguments

Let’s see how stabilize_video.rb parses the mandatory arguments:

stabilize_video.rb
require 'colorator'
require_relative 'stabilize_video/version'
require_relative 'options'

# Require all Ruby files in 'lib/', except this file
Dir[File.join(__dir__, '*.rb')].each do |file|
  require file unless file.end_with?('/stabilize_video.rb')
end

def main
  options = parse_options
  help 'Video file name must be provided.' if ARGV.empty?
  help "Too many parameters specified.\n#{ARGV}" if ARGV.length > 1
  video_in = ARGV[0]
  video_out = "#{File.dirname video_in}/stabilized_#{File.basename video_in}"
  StablizeVideo.new(video_in, video_out, **options).stabilize
end

main

Here are some notes to help you understand the above code:

  • Usage of colorator or rainbow to output colored strings helps readability, but is not required.
  • Because OptionParser removes each argument from ARGV that it recognizes, when it finishes all that should be left on the command line are the mandatory arguments. The main method calls parse_options, which as we know calls OptionParser, and then ensures that a mandatory filename parameter is provided.
  • parse_options returns a hash of name/value pairs, which can optionally be passed when doubly dereferenced with two asterisks (**options). This is done in the highlighted code above when creating a new StablizeVideo instance.

Passing Options

In the following code, the optional values returned by parse_options are provided to the StablizeVideo.initialize method. Once again, double asterisks are used.

Portion of stabilize.rb
  def initialize(video_in, video_out, **options)
    @options   = options
    @loglevel  = "-loglevel #{options[:loglevel]}"
    @loglevel += ' -stats' unless options[:loglevel] == 'quiet'
    @shakiness = "shakiness=#{options[:shake]}"
    @video_in  = MSUtil.expand_env video_in
    @video_out = MSUtil.expand_env video_out
    unless File.exist?(@video_in)
      printf "Error: file #{@video_in} does not exist.\n"
      exit 2
    end
    unless File.readable? @video_in
      printf "Error: #{@video_in} cannot be read.\n"
      exit 2
    end
    return unless File.exist?(@video_out) && !options.key?(:overwrite)

    printf "Error: #{@video_out} already exists.\n"
    exit 3
  end

Notice in the above code that:

  • The value of the -l / --loglevel option is obtained from options[:loglevel].
  • The value of the -s / --shake option is obtained from options[:shake].
  • If the user specified the -f (--overwrite) option, that is detected by options.key?(:overwrite).

Hand-written Help Text

I find that using the automatically generated help text results in a more complex program for little gain, because there are so many moving parts to keep track of. Explicitly writing the help method is a more maintainable way of showing the user what they need to know.

The help method below generates the help text, which might be preceded with an error message.

Portion of options.rb
def help(msg = nil)
  printf "Error: #{msg}\n\n".yellow unless msg.nil?
  msg = <<~END_HELP
    stabilize: Stabilizes a video using the FFmpeg vidstabdetect and vidstabtransform filters.

    Syntax: stabilize [Options] PATH_TO_VIDEO

    Options:
      -f Overwrite output file if present
      -h Show this help message
      -s Shakiness compensation 1..10 (default 5)
      -v Verbosity; one of: #{VERBOSITY.join ', '}
      -z Zoom percentage (computed if not specified)

    See:
      https://www.ffmpeg.org/ffmpeg-filters.html#vidstabdetect-1
      https://www.ffmpeg.org/ffmpeg-filters.html#toc-vidstabtransform-1
  END_HELP
  printf msg.cyan
  exit 1
end

A Ruby squiggly heredoc is used to store a multiline string as the help text.

Running the Program

Now let's run the above program and view the generated help text:

Shell
$ stabilize -h
stabilize -h
  stabilize: Stabilizes a video using the FFmpeg vidstabdetect and vidstabtransform filters.

  Syntax: stabilize [Options] PATH_TO_VIDEO

  Options:
    -f Overwrite output file if present
    -h Show this help message
    -s Shakiness compensation 1..10 (default 5)
    -v Verbosity; one of: trace, debug, verbose, info, warning, error, fatal, panic, quiet
    -z Zoom percentage (computed if not specified)

  See:
    https://www.ffmpeg.org/ffmpeg-filters.html#vidstabdetect-1
    https://www.ffmpeg.org/ffmpeg-filters.html#toc-vidstabtransform-1 

HighLine

BTW, highline is a gem that is often found in CLIs that are built with OptionParser. The agree method is particularly useful.

Both the agree and ask methods have an undocumented feature: Putting a space at the end of the question string suppresses the newline between the question and the answer.

The optional character parameter causes the first character that the user types to be grabbed and processed without requiring Enter.

my_cli.rb
$ irb
irb(main):001> require 'highline'
=> true
irb(main):002* begin
irb(main):003*   printf "Work work work"
irb(main):004> end while HighLine.agree "\nAll done! Do you want to do it again? ", character = true
Work work work
All done! Do you want to do it again?
Please enter "yes" or "no".
All done! Do you want to do it again? y
Work work work
All done! Do you want to do it again? y
Work work work
All done! Do you want to do it again? n
=> nil
irb(main):005> 


* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.