How do you specify a required switch (not argument) with Ruby OptionParser?

45,836

Solution 1

I am assuming you are using optparse here, although the same technique will work for other option parsing libraries.

The simplest method is probably to parse the parameters using your chosen option parsing library and then raise an OptionParser::MissingArgument Exception if the value of host is nil.

The following code illustrates

#!/usr/bin/env ruby
require 'optparse'

options = {}

optparse = OptionParser.new do |opts|
  opts.on('-h', '--host HOSTNAME', "Mandatory Host Name") do |f|
    options[:host] = f
  end
end

optparse.parse!

#Now raise an exception if we have not found a host option
raise OptionParser::MissingArgument if options[:host].nil?


puts "Host = #{options[:host]}"

Running this example with a command line of

./program -h somehost

simple displays "Host = somehost"

Whilst running with a missing -h and no file name produces the following output

./program:15: missing argument:  (OptionParser::MissingArgument)

And running with a command line of ./program -h produces

/usr/lib/ruby/1.8/optparse.rb:451:in `parse': missing argument: -h (OptionParser::MissingArgument)
  from /usr/lib/ruby/1.8/optparse.rb:1288:in `parse_in_order'
  from /usr/lib/ruby/1.8/optparse.rb:1247:in `catch'
  from /usr/lib/ruby/1.8/optparse.rb:1247:in `parse_in_order'
  from /usr/lib/ruby/1.8/optparse.rb:1241:in `order!'
  from /usr/lib/ruby/1.8/optparse.rb:1332:in `permute!'
  from /usr/lib/ruby/1.8/optparse.rb:1353:in `parse!'
  from ./program:13

Solution 2

An approach using optparse that provides friendly output on missing switches:

#!/usr/bin/env ruby
require 'optparse'

options = {}

optparse = OptionParser.new do |opts|
  opts.on('-f', '--from SENDER', 'username of sender') do |sender|
    options[:from] = sender
  end

  opts.on('-t', '--to RECIPIENTS', 'comma separated list of recipients') do |recipients|
    options[:to] = recipients
  end

  options[:number_of_files] = 1
  opts.on('-n', '--num_files NUMBER', Integer, "number of files to send (default #{options[:number_of_files]})") do |number_of_files|
    options[:number_of_files] = number_of_files
  end

  opts.on('-h', '--help', 'Display this screen') do
    puts opts
    exit
  end
end

begin
  optparse.parse!
  mandatory = [:from, :to]                                         # Enforce the presence of
  missing = mandatory.select{ |param| options[param].nil? }        # the -t and -f switches
  unless missing.empty?                                            #
    raise OptionParser::MissingArgument.new(missing.join(', '))    #
  end                                                              #
rescue OptionParser::InvalidOption, OptionParser::MissingArgument      #
  puts $!.to_s                                                           # Friendly output when parsing fails
  puts optparse                                                          #
  exit                                                                   #
end                                                                      #

puts "Performing task with options: #{options.inspect}"

Running without the -t or -f switches shows the following output:

Missing options: from, to
Usage: test_script [options]
    -f, --from SENDER                username of sender
    -t, --to RECIPIENTS              comma separated list of recipients
    -n, --num_files NUMBER           number of files to send (default 1)
    -h, --help

Running the parse method in a begin/rescue clause allows friendly formatting upon other failures such as missing arguments or invalid switch values, for instance, try passing a string for the -n switch.

Solution 3

I came up with a clear and concise solution that sums up your contributions. It raises an OptionParser::MissingArgument exception with the missing arguments as a message. This exception is catched in the rescue block along with the rest of exceptions coming from OptionParser.

#!/usr/bin/env ruby
require 'optparse'

options = {}

optparse = OptionParser.new do |opts|
  opts.on('-h', '--host hostname', "Host name") do |host|
    options[:host] = host
  end
end

begin
  optparse.parse!
  mandatory = [:host]
  missing = mandatory.select{ |param| options[param].nil? }
  raise OptionParser::MissingArgument, missing.join(', ') unless missing.empty?
rescue OptionParser::ParseError => e
  puts e
  puts optparse
  exit
end

Running this example:

 ./program            
missing argument: host
Usage: program [options]
    -h, --host hostname              Host name

Solution 4

If you do something like this:

opts.on('-h', '--host',
          'required host name [STRING]') do |h|
    someoptions[:host] = h || nil
  end

Then the someoptions[:host] will either be the value from the commandline or nil (if you don't supply --host and/or no value after --host) and you can test for it easily (and conditionally fail) after the parse:

fail "Hostname not provided" unless someoptions[:host]

Solution 5

The answer from unknown (google) is good, but contains a minor error.

rescue OptionParser::InvalidArgument, OptionParser::MissingArgument

should be

OptionParser::InvalidOption, OptionParser::MissingArgument

Otherwise, optparse.parse! will trigger the standard error output for OptionParser::InvalidOption, not the custom message.

Share:
45,836
Teflon Ted
Author by

Teflon Ted

Married to Java. Sleeping with Ruby.

Updated on March 19, 2020

Comments

  • Teflon Ted
    Teflon Ted over 4 years

    I'm writing a script and I want to require a --host switch with value, but if the --host switch isn't specified, I want the option parsing to fail.

    I can't seem to figure out how to do that. The docs seem to only specify how to make the argument value mandatory, not the switch itself.