Parse command line arguments in a Ruby script

I want to call a Ruby script from the command line, and pass in parameters that are key/value pairs.

Command line call:

$ ruby my_script.rb --first_name=donald --last_name=knuth

my_script.rb:

puts args.first_name + args.last_name

What is the standard Ruby way to do this? In other languages I usually have to use an option parser. In Ruby I saw we have ARGF.read, but that does not seem to work key/value pairs like in this example.

OptionParser looks promising, but I can't tell if it actually supports this case.


Ruby's built-in OptionParser does this nicely. Combine it with OpenStruct and you're home free:

require 'optparse'

options = {}
OptionParser.new do |opt|
  opt.on('--first_name FIRSTNAME') { |o| options[:first_name] = o }
  opt.on('--last_name LASTNAME') { |o| options[:last_name] = o }
end.parse!

puts options

options will contain the parameters and values as a hash.

Saving and running that at the command line with no parameters results in:

$ ruby test.rb
{}

Running it with parameters:

$ ruby test.rb --first_name=foo --last_name=bar
{:first_name=>"foo", :last_name=>"bar"}

That example is using a Hash to contain the options, but you can use an OpenStruct which will result in usage like your request:

require 'optparse'
require 'ostruct'

options = OpenStruct.new
OptionParser.new do |opt|
  opt.on('-f', '--first_name FIRSTNAME', 'The first name') { |o| options.first_name = o }
  opt.on('-l', '--last_name LASTNAME', 'The last name') { |o| options.last_name = o }
end.parse!

puts options.first_name + ' ' + options.last_name

$ ruby test.rb --first_name=foo --last_name=bar
foo bar

It even automatically creates your -h or --help option:

$ ruby test.rb -h
Usage: test [options]
        --first_name FIRSTNAME
        --last_name LASTNAME

You can use short flags too:

require 'optparse'

options = {}
OptionParser.new do |opt|
  opt.on('-f', '--first_name FIRSTNAME') { |o| options[:first_name] = o }
  opt.on('-l', '--last_name LASTNAME') { |o| options[:last_name] = o }
end.parse!

puts options

Running that through its paces:

$ ruby test.rb -h
Usage: test [options]
    -f, --first_name FIRSTNAME
    -l, --last_name LASTNAME
$ ruby test.rb -f foo --l bar
{:first_name=>"foo", :last_name=>"bar"}

It's easy to add inline explanations for the options too:

OptionParser.new do |opt|
  opt.on('-f', '--first_name FIRSTNAME', 'The first name') { |o| options[:first_name] = o }
  opt.on('-l', '--last_name LASTNAME', 'The last name') { |o| options[:last_name] = o }
end.parse!

and:

$ ruby test.rb -h
Usage: test [options]
    -f, --first_name FIRSTNAME       The first name
    -l, --last_name LASTNAME         The last name

OptionParser also supports converting the parameter to a type, such as an Integer or an Array. Refer to the documentation for more examples and information.

You should also look at the related questions list to the right:

  • "Really Cheap Command-Line Option Parsing in Ruby"
  • "Pass variables to Ruby script via command line"

Based on the answer by @MartinCortez here's a short one-off that makes a hash of key/value pairs, where the values must be joined with an = sign. It also supports flag arguments without values:

args = Hash[ ARGV.join(' ').scan(/--?([^=\s]+)(?:=(\S+))?/) ]

…or alternatively…

args = Hash[ ARGV.flat_map{|s| s.scan(/--?([^=\s]+)(?:=(\S+))?/) } ]

Called with -x=foo -h --jim=jam it returns {"x"=>"foo", "h"=>nil, "jim"=>"jam"} so you can do things like:

puts args['jim'] if args.key?('h')
#=> jam

While there are multiple libraries to handle this—including GetoptLong included with Ruby—I personally prefer to roll my own. Here's the pattern I use, which makes it reasonably generic, not tied to a specific usage format, and flexible enough to allow intermixed flags, options, and required arguments in various orders:

USAGE = <<ENDUSAGE
Usage:
   docubot [-h] [-v] [create [-s shell] [-f]] directory [-w writer] [-o output_file] [-n] [-l log_file]
ENDUSAGE

HELP = <<ENDHELP
   -h, --help       Show this help.
   -v, --version    Show the version number (#{DocuBot::VERSION}).
   create           Create a starter directory filled with example files;
                    also copies the template for easy modification, if desired.
   -s, --shell      The shell to copy from.
                    Available shells: #{DocuBot::SHELLS.join(', ')}
   -f, --force      Force create over an existing directory,
                    deleting any existing files.
   -w, --writer     The output type to create [Defaults to 'chm']
                    Available writers: #{DocuBot::Writer::INSTALLED_WRITERS.join(', ')}
   -o, --output     The file or folder (depending on the writer) to create.
                    [Default value depends on the writer chosen.]
   -n, --nopreview  Disable automatic preview of .chm.
   -l, --logfile    Specify the filename to log to.

ENDHELP

ARGS = { :shell=>'default', :writer=>'chm' } # Setting default values
UNFLAGGED_ARGS = [ :directory ]              # Bare arguments (no flag)
next_arg = UNFLAGGED_ARGS.first
ARGV.each do |arg|
  case arg
    when '-h','--help'      then ARGS[:help]      = true
    when 'create'           then ARGS[:create]    = true
    when '-f','--force'     then ARGS[:force]     = true
    when '-n','--nopreview' then ARGS[:nopreview] = true
    when '-v','--version'   then ARGS[:version]   = true
    when '-s','--shell'     then next_arg = :shell
    when '-w','--writer'    then next_arg = :writer
    when '-o','--output'    then next_arg = :output
    when '-l','--logfile'   then next_arg = :logfile
    else
      if next_arg
        ARGS[next_arg] = arg
        UNFLAGGED_ARGS.delete( next_arg )
      end
      next_arg = UNFLAGGED_ARGS.first
  end
end

puts "DocuBot v#{DocuBot::VERSION}" if ARGS[:version]

if ARGS[:help] or !ARGS[:directory]
  puts USAGE unless ARGS[:version]
  puts HELP if ARGS[:help]
  exit
end

if ARGS[:logfile]
  $stdout.reopen( ARGS[:logfile], "w" )
  $stdout.sync = true
  $stderr.reopen( $stdout )
end

# etc.

I personally use Docopt. This is much more clear, maintainable and easy to read.

Have a look at the Ruby implementation's documentation for examples. The usage is really straightforward.

gem install docopt

Ruby code:

doc = <<DOCOPT
My program who says hello

Usage:
  #{__FILE__} --first_name=<first_name> --last_name=<last_name>
DOCOPT

begin
  args = Docopt::docopt(doc)
rescue Docopt::Exit => e
  puts e.message
  exit
end

print "Hello #{args['--first_name']} #{args['--last_name']}"

Then calling:

$ ./says_hello.rb --first_name=Homer --last_name=Simpsons
Hello Homer Simpsons

And without arguments:

$ ./says_hello.rb
Usage:
  says_hello.rb --first_name=<first_name> --last_name=<last_name>

There is a number of command line arguments parsers in Ruby:

  • GetoptLong - Included in stdlib
  • OptionParser - No longer part of stdlib, since Ruby 3.0.0 converted to a separate optparse gem
  • slop
  • optimist
  • and many more...

Personally I'd choose slop or optimist, those are not part of standard Ruby installation.

gem install slop

But it offers simplicity and code readability. Assuming slightly more complex example with required arguments and default values:

require 'slop'

begin
  opts = Slop.parse do |o|
    o.int '-a', '--age', 'Current age', default: 42
    o.string '-f', '--first_name', 'The first name', required: true
    o.string '-l', '--last_name', 'The last name', required: true
    o.bool '-v', '--verbose', 'verbose output', default: false
    o.on '-h','--help', 'print the help' do
      puts o
      exit
    end
  end

  p opts.to_hash
rescue Slop::Error => e
  puts e.message
end

optimist formerly known as trollop, it's very easy to ready, with minimum boilerplate code:

gem install optimist
require 'optimist'

opts = Optimist::options do
  opt :verbose, "verbose mode"
  opt :first_name, "The first name", type: :string, required: true
  opt :last_name, "The last name", type: :string, required: true
  opt :age, "Current age", default: 42
end

p opts

Similar example using OptionParser:

#!/usr/bin/env ruby

require 'optparse'
require 'ostruct'

begin
  options = OpenStruct.new
  OptionParser.new do |opt|
    opt.on('-a', '--age AGE', 'Current age') { |o| options.age = o }
    opt.on('-f', '--first_name FIRSTNAME', 'The first name') { |o| options.first_name = o }
    opt.on('-l', '--last_name LASTNAME', 'The last name') { |o| options.last_name = o }
    opt.on('-v', '--verbose', 'Verbose output') { |o| options.verbose = true }
  end.parse!

  options[:age] = 42 if options[:age].nil?
  raise OptionParser::MissingArgument.new('--first_name') if options[:first_name].nil?
  raise OptionParser::MissingArgument.new('--last_name') if options[:last_name].nil?
  options[:verbose] = false if options[:verbose].nil?

rescue OptionParser::ParseError => e
  puts e.message
  exit
end

GetoptLong parsing is even more complicated:

require 'getoptlong'

opts = GetoptLong.new(
  [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
  [ '--first_name', '-f', GetoptLong::REQUIRED_ARGUMENT ],
  [ '--last_name', '-l', GetoptLong::REQUIRED_ARGUMENT ],
  [ '--age','-a', GetoptLong::OPTIONAL_ARGUMENT ],
  [ '--verbose','-v', GetoptLong::OPTIONAL_ARGUMENT ]
)
begin
  options = {}
  options[:verbose] = false
  options[:age] = 42
  opts.each do |opt, arg|
    case opt
    when '--help'
        puts <<-EOF
  usage: ./getlongopts.rb [options]

      -a, --age         Current age
      -f, --first_name  The first name
      -l, --last_name   The last name
      -v, --verbose     verbose output
      -h, --help        print the help

        EOF
    when '--first_name'
      options[:first_name] = arg
    when '--last_name'
      options[:last_name] = arg
    when '--age'
      options[:age] = arg.to_i
    when '--verbose'
      options[:verbose] = arg
    else
      puts "unknown option `#{opt}`"
      exit 1
    end
  end

  raise GetoptLong::MissingArgument.new('Missing argument --first_name') if options[:first_name].nil?
  raise GetoptLong::MissingArgument.new('Missing argument --last_name') if options[:last_name].nil?

rescue GetoptLong::Error => e
  puts e.message
  exit
end

puts options

Command line arguments was never meant to be a rocket science task, spend your time on reading/writing more useful code :)