views:

1312

answers:

5

Say I'm writing a script and I want to require a --host switch (with value of course) 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.

+4  A: 

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
Steve Weet
It stinks that's not a native feature of the library - it's not very DRY if you have a few more required switches. Thanks.
Teflon Ted
Rather than raise a cryptic exception, I prefer to exit with a non-zero status by invoking `Kernel.abort`. It takes an optional argument which you can use to specify the reason for aborting.
Tate Johnson
Agreed Ted. It's not DRY at all and should be ashamed that it isn't. My very first commandline app I needed a mandatory switch so it's unconscionable that this isn't included.
Mike Bethany
+3  A: 

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                                                                                                                        
  if not missing.empty?                                            #                                                                                                                                             
    puts "Missing options: #{missing.join(', ')}"                  #                                                                                                                                             
    puts optparse                                                  #                                                                                                                                             
    exit                                                           #                                                                                                                                             
  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 ouptut:

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)

volund
fixed according to neilfws' comments
volund
That's not bad but it's still not very DRY. You have to do a lot of work at the end there and have to specify your switches in two places. Check out my fix below that is much simpler and much DRY'er. Also on my blog: http://picklepumpers.com/wordpress/?p=949
Mike Bethany
A: 

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.

neilfws
A: 

Thank you volund for the good solution

jhodge
+2  A: 

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] = option
end

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
  exit
end

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 ruby
require '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
  end

end.parse!

optparse_required_switches.rb

# Add required switches to OptionParser
class 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
  end

end
Mike Bethany