Ruby optparse odd behavior with exception handling

510 views Asked by At

I was working on making a CLI ruby tool a bit more robust with error handling of the CLI component of the tool. I'm using optparse and the documentation shows that flags can have mandatory and optional arguments. I'm just seeing some odd/annoying behavior.

For one case where the argument of the flag is mandatory (using the equal sign), I was trying to make it fail but it still worked, it just grabbed the next flag ('-u') as the argument to that flag instead which causes the rest of the parsing to barf in a less than ideal way. I guess this is kind of OK since '-....' can be valid but since I have it using the equal sign for setting the value of the switch, I'd assume that the 'space' style assignment wouldn't work.

op = OptionParser.new do |x|
  x.on("-u", "--user=USER", "user flag") do |user|  options[:user] = user end
  x.on("-d", "--db=DATABASE", "database flag") do |db|  options[:db] = db end
end

If I pass the following CLI input:

myprog -u -d mydb positionalarg

Then during parsing, it sets options[:user] to be -d, and options[:db] is nil since it is not encountered and there are 2 positional arguments instead of 1. Obviously this is a user error but I want to catch it and display a real error and not just return an error (in the case of the tool) that only one positional argument should be passed from the following list: ... The true issue is that the -u flag is missing it's required argument so given that optparse has a ArgumentMissing exception, I'd assume that's what .parse! would throw.

However, with a flag that has an optional argument (using the equal sign), it is in fact optional if you do: -a -b=something but this does not work: -a something -b=somethingelse. The only way to force the value is to use the equal sign: -a=something -b=somethingelse. Given the previous behavior with the mandatory argument with an equal sign, I'd wouldn't think would be the case. Example:

op = OptionParser.new do |x|
  x.on("-u", "--user[=USER]", "user flag") do |user|  options[:user] = user unless user.nil? end
  x.on("-d", "--db=DATABASE", "database flag") do |db|  options[:db] = db end
end

So with parsing:

myprog -u -d mydb positionalarg

Then options[:user] is nil (ok) and options[:db] is mydb and there is one positional arg left over.

With this parsing:

myprog -u myuser -d mydb positionalarg

Then options[:user] is nil (not ok) and options[:db] is mydb and there are two positional args left over: myuser and positionalarg (not ok). My error checking, again, barfs with the positional arg count. It seems that if with mandatory flag arguments, that if both space and = works, then for optional flag args, it should be the same but this is not the case.

The other issue is that the flags with optional args (using space) are fine except if they are at the end of the command and then takes the positional argument as the flags argument.

Example:

op = OptionParser.new do |x|
  x.on("-u", "--user [USER]", "user flag") do |user|  options[:user] = user unless user.nil? end
  x.on("-d", "--db=DATABASE", "database flag") do |db|  options[:db] = db end
end

So with parsing:

myprog -d mydb -u positionalarg

Then options[:db] is mydb (ok) and options[:user] is positionalarg and there are no positional arguments left over. Note that -d mydb works with the space even those I specify it with an equals sign.

Seems a lot of people do ruby CLIs by doing optparse .parse! and take the remaining entries in ARGV as the positional arguments, but I'm thinking it might be better to strip the positional arguments off the far end first before passing ARGV to optparse (except this fails in the event of variable number of positional arguments.

I'm confident I can program around all this but I'd prefer not to if there are ways to do it in optparse that I'm unaware of.

Maybe the best thing would be to avoid flags with optional arguments :), but any advice would be appreciated.

1

There are 1 answers

1
knut On

Why do you use the = in the definition of the options? The example in the documentation don't use them.

If I define this MWE as test.rb:

require 'optparse'

puts "\n=Call with #{ARGV.inspect}"
options = {}

op = OptionParser.new do |x|
  x.on("-u", "--user [USER]", "user flag") do |user|  options[:user] = user unless user.nil? end
  x.on("-d", "--db DATABASE", "database flag") do |db|  options[:db] = db end
end

op.parse!

puts "Options: #{options.inspect}"
puts "ARGV     #{ARGV.inspect}"

And call it with this batfile (Windows, remove the @echo off for Linux):

@echo off
test.rb -h
test.rb -u -d mydb positionalarg
test.rb -u myuser -d mydb positionalarg
test.rb --user=myuser -d mydb positionalarg

I get:

=Call with ["-h"]
Usage: test [options]
    -u, --user [USER]                user flag
    -d, --db DATABASE                database flag

=Call with ["-u", "-d", "mydb", "positionalarg"]
Options: {:db=>"mydb"}
ARGV     ["positionalarg"]

=Call with ["-u", "myuser", "-d", "mydb", "positionalarg"]
Options: {:user=>"myuser", :db=>"mydb"}
ARGV     ["positionalarg"]

=Call with ["--user=myuser", "-d", "mydb", "positionalarg"]
Options: {:user=>"myuser", :db=>"mydb"}
ARGV     ["positionalarg"]