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