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

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


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

#!/usr/bin/env rubyrequire '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  endendbegin  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, toUsage: 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.


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 rubyrequire 'optparse'options = {}optparse = OptionParser.new do |opts|  opts.on('-h', '--host HOSTNAME', "Mandatory Host Name") do |f|    options[:host] = f  endendoptparse.parse!#Now raise an exception if we have not found a host optionraise 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


I turned this into a gem you can download and install from rubygems.org:

gem install pickled_optparse

And you can checkout the updated project source code on github:
http://github.com/PicklePumpers/pickled_optparse

-- Older post info --

This was really, really bugging me so I fixed it and kept the usage super DRY.

To make a switch required just add a :required symbol anywhere in the array of options like so:

opts.on("-f", "--foo [Bar]", String, :required, "Some required option") do |option|  @options[:foo] = optionend

Then at the end of your OptionParser block add one of these to print out the missing switches and the usage instructions:

if opts.missing_switches?  puts opts.missing_switches  puts opts  exitend

And finally to make it all work you need to add the following "optparse_required_switches.rb" file to your project somewhere and require it when you do your command line parsing.

I wrote up a little article with an example on my blog:http://picklepumpers.com/wordpress/?p=949

And here's the modified OptionParser file with an example of its usage:

required_switches_example.rb

#!/usr/bin/env rubyrequire 'optparse'require_relative 'optparse_required_switches'# Configure options based on command line options@options = {}OptionParser.new do |opts|  opts.banner = "Usage: test [options] in_file[.srt] out_file[.srt]"  # Note that :required can be anywhere in the parameters  # Also note that OptionParser is bugged and will only check   # for required parameters on the last option, not my bug.  # required switch, required parameter  opts.on("-s Short", String, :required, "a required switch with just a short") do |operation|    @options[:operation] = operation  end  # required switch, optional parameter  opts.on(:required, "--long [Long]", String, "a required switch with just a long") do |operation|    @options[:operation] = operation  end  # required switch, required parameter  opts.on("-b", "--both ShortAndLong", String, "a required switch with short and long", :required) do |operation|    @options[:operation] = operation  end  # optional switch, optional parameter  opts.on("-o", "--optional [Whatever]", String, "an optional switch with short and long") do |operation|    @options[:operation] = operation  end  # Now we can see if there are any missing required   # switches so we can alert the user to what they   # missed and how to use the program properly.  if opts.missing_switches?    puts opts.missing_switches    puts opts    exit  endend.parse!

optparse_required_switches.rb

# Add required switches to OptionParserclass OptionParser  # An array of messages describing the missing required switches  attr_reader :missing_switches  # Convenience method to test if we're missing any required switches  def missing_switches?    !@missing_switches.nil?  end  def make_switch(opts, block = nil)    short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], []    ldesc, sdesc, desc, arg = [], [], []    default_style = Switch::NoArgument    default_pattern = nil    klass = nil    n, q, a = nil    # Check for required switches    required = opts.delete(:required)    opts.each do |o|      # argument class      next if search(:atype, o) do |pat, c|        klass = notwice(o, klass, 'type')        if not_style and not_style != Switch::NoArgument          not_pattern, not_conv = pat, c        else          default_pattern, conv = pat, c        end      end      # directly specified pattern(any object possible to match)      if (!(String === o || Symbol === o)) and o.respond_to?(:match)        pattern = notwice(o, pattern, 'pattern')        if pattern.respond_to?(:convert)          conv = pattern.method(:convert).to_proc        else          conv = SPLAT_PROC        end        next      end      # anything others      case o        when Proc, Method          block = notwice(o, block, 'block')        when Array, Hash          case pattern            when CompletingHash            when nil              pattern = CompletingHash.new              conv = pattern.method(:convert).to_proc if pattern.respond_to?(:convert)            else              raise ArgumentError, "argument pattern given twice"          end          o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}}        when Module          raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4))        when *ArgumentStyle.keys          style = notwice(ArgumentStyle[o], style, 'style')        when /^--no-([^\[\]=\s]*)(.+)?/          q, a = $1, $2          o = notwice(a ? Object : TrueClass, klass, 'type')          not_pattern, not_conv = search(:atype, o) unless not_style          not_style = (not_style || default_style).guess(arg = a) if a          default_style = Switch::NoArgument          default_pattern, conv = search(:atype, FalseClass) unless default_pattern          ldesc << "--no-#{q}"          long << 'no-' + (q = q.downcase)          nolong << q        when /^--\[no-\]([^\[\]=\s]*)(.+)?/          q, a = $1, $2          o = notwice(a ? Object : TrueClass, klass, 'type')          if a            default_style = default_style.guess(arg = a)            default_pattern, conv = search(:atype, o) unless default_pattern          end          ldesc << "--[no-]#{q}"          long << (o = q.downcase)          not_pattern, not_conv = search(:atype, FalseClass) unless not_style          not_style = Switch::NoArgument          nolong << 'no-' + o        when /^--([^\[\]=\s]*)(.+)?/          q, a = $1, $2          if a            o = notwice(NilClass, klass, 'type')            default_style = default_style.guess(arg = a)            default_pattern, conv = search(:atype, o) unless default_pattern          end          ldesc << "--#{q}"          long << (o = q.downcase)        when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/          q, a = $1, $2          o = notwice(Object, klass, 'type')          if a            default_style = default_style.guess(arg = a)            default_pattern, conv = search(:atype, o) unless default_pattern          end          sdesc << "-#{q}"          short << Regexp.new(q)        when /^-(.)(.+)?/          q, a = $1, $2          if a            o = notwice(NilClass, klass, 'type')            default_style = default_style.guess(arg = a)            default_pattern, conv = search(:atype, o) unless default_pattern          end          sdesc << "-#{q}"          short << q        when /^=/          style = notwice(default_style.guess(arg = o), style, 'style')          default_pattern, conv = search(:atype, Object) unless default_pattern        else          desc.push(o)      end    end    default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern    if !(short.empty? and long.empty?)      s = (style || default_style).new(pattern || default_pattern, conv, sdesc, ldesc, arg, desc, block)    elsif !block      if style or pattern        raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller)      end      s = desc    else      short << pattern      s = (style || default_style).new(pattern, conv, nil, nil, arg, desc, block)    end    # Make sure required switches are given    if required && !(default_argv.include?("-#{short[0]}") || default_argv.include?("--#{long[0]}"))        @missing_switches ||= [] # Should be placed in initialize if incorporated into Ruby proper        # This is more clear but ugly and long.        #missing = "-#{short[0]}" if !short.empty?        #missing = "#{missing} or " if !short.empty? && !long.empty?        #missing = "#{missing}--#{long[0]}" if !long.empty?        # This is less clear and uglier but shorter.        missing = "#{"-#{short[0]}" if !short.empty?}#{" or " if !short.empty? && !long.empty?}#{"--#{long[0]}" if !long.empty?}"        @missing_switches << "Missing switch: #{missing}"    end    return s, short, long,      (not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style),      nolong  endend