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.
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"]
Related
This code is intended to check whether the user entered an option with the command:
require 'optparse'
ARGV << '-h' if ARGV.empty?
options = {}
OptionParser.new do |parser|
parser.banner = "Usage: myruby.rb [options]"
parser.on("-h", "--help", "Help myruby") do | |
puts parser
exit
end
parser.on("-p", "--people PEOPLE", "PPPPPPPPPP") do |v|
options[:pppp] = v
end
parser.on("-c", "--coordinate COORDINATE", "ccccccccc") do |x|
options[:coordinate] = x
end
end.parse!
# Start my program from this line
unless options[:pppp] && options[:coordinate]
puts "Exit OK because missing both (option and argument) p,c"
exit
end
puts "It work if only run myruby.rb -p argument_P -c argument_c"
I just found an error. If the user enters only one but not both required ARGV (-p -c).
I can check and exit from my application, but I want to filter ARGV by exiting to assign ARGV << 'h'.
What is the best way?
updated 1: Added unless case before run my program problem : Worked as
asked, but error when -p or -c missing argument. example : ruby
thiscode.rb -p bababa -c error : rb:17:in `': missing
argument (OptionParser::MissingArgument)
Just explicitly check the presence of both after options are parsed:
unless options[:pppp] && options[:coordinate]
puts USAGE # or do whatever else
exit
end
I am working with OptionParser for the first time.
What I would like to know, is how I can make OptionParser handle arguments that are not prefixed with a certain flagname. I want to be able to write a statement like:
myscript.rb -d someoption -b someotheroption filename
where filename is the name of the file I want to work on. It is not prefixed by any option flag. How can I parse commands like the one above with OptionParser, and get a reference to filename?
OptionParser specifically handles options - that is, things starting with dashes. After it parses, the remaining arguments are left in ARGV. You can check for your filename there and exit with an error if it's missing.
With a slight modification on their minimal example,
require 'optparse'
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: example.rb [options]"
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
options[:verbose] = v
end
end.parse!
p options
p ARGV
p "Where is my hat?!" if ARGV.length == 0
You get this:
$ ruby parse.rb
{}
[]
"Where is my hat?!"
$ ruby parse.rb hat
{}
["hat"]
$ ruby parse.rb -v hat
{:verbose=>true}
["hat"]
I have such a simple code in Ruby (test.rb):
#! /usr/bin/env ruby
require 'optparse'
OptionParser.new do |option|
option.on("--sort", "Sort data") do
puts "--sort passed"
end
end.parse!
then I run it: ./test.rb -s and got:
--sort passed
Have I missed something?
I want the only --sort (long) option works, not the short one.
How do I get it?
I found the code which causes this behavior, in optparse.rb, lines 1378-1380:
# if no short options match, try completion with long
# options.
sw, = complete(:long, opt)
If you don't like that behavior, it seems your best option is to create a copy of optparse.rb within your project, remove the offending rescue InvalidOption clause within the copy, and load that rather than the standard library's version.
It is interesting behaviour that if you define the similar long option that begins with the same letter, in the example is s. It does not allow to use -s key with exception OptionParser::AmbiguousOption, but it seems that there no a way to disable the short option for OptionParser without invading into its code:
#! /usr/bin/env ruby
require 'optparse'
OptionParser.new do |option|
option.on("--sport", "Sport data") do
puts "--sport passed"
end
option.on("--sort", "Sort data") do
puts "--sort passed"
end
end.parse!
This is the expanded version of on method:
OptionParser.new do |option|
opts = [ "--sort", "Sort data" ]
sw = option.make_switch(opts)
block = proc { puts "--sort passed" }
sw[0].instance_variable_set :#block, block
option.top.append *sw
p sw
# => [#<OptionParser::Switch::NoArgument:0x806c770 #pattern=/.*/m, #conv=#<Proc:0x806dd8c#/home/malo/.rvm/rubies/ruby-1.9.3-p448/lib/ruby/1.9.1/optparse.rb:1617>, #short=[], #long=["--sort"], #arg=nil, #desc=["Sort data"], #block=#<Proc:0x806c70c#./1.rb:8>>, [], ["sort"], nil, []]
end.parse!
# => --sort passed when ./1.rb --sort and ./1.rb -s
It is interesting that #short variable is empty but the app reacts on -s key.
I would prefer to use micro-optparse gem. Use it as follows:
Gemfile
gem 'micro-optparse', :git => 'https://github.com/3aHyga/micro-optparse.git', :branch => 'no-short' # for now it is available only from git repo
ruby_script.rb
require 'micro-optparse'
options = Parser.new do |p|
p.banner = "This is a fancy script, for usage see below"
p.option :sport, "sport", :default => "Sport", :short => "p"
p.option :sort, "sort", :default => "Sort", :short => false
end.process!
p options
Simulation:
$ bundle exec ./1.rb --sort 111
{:sport=>"Sport", :sort=>"111"}
$ bundle exec ./1.rb -s 111
ambiguous option: -s
$ bundle exec ./1.rb -p 111
{:sport=>"111", :sort=>"Sort"}
You can reopen OptionParser::OptionMap to disable completions with:
class OptionParser::OptionMap
def complete(key, icase = false, pat = nil)
# disable completions
nil
end
end
This will disable the predefined behavior of searching for stuff to complete.
My program has a parameter '--sort' which may accept arguments like '-s', 's' , '+s', etc
In that case you can pass an array of valid arguments to your option:
require 'optparse'
OptionParser.new do |option|
option.on("--sort TYPE", %w(-s s +s), "Sort data") do |type|
puts "--sort passed with argument #{type}"
end
end.parse!
Usage:
$ ./test.rb --sort -s
--sort passed with argument -s
$ ./test.rb --sort s
--sort passed with argument s
$ ./test.rb --sort +s
--sort passed with argument +s
Note that you can still use the shorthand -s:
$ ./test.rb -s -s
--sort passed with argument -s
$ ./test.rb -s s
--sort passed with argument s
$ ./test.rb -s +s
--sort passed with argument +s
From the documentation, it appears that this isn't possible.
The #on method uses the syntax of #make_switch, which is described here. The whole documentation makes no mention of being able to turn long or short variables on or off.
However, is this really a problem? The convention is that options are accessible via long and short names, and forcing a change in that behaviour might frustrate your users.
If you really don't want to allow short names, the best option would be to look at some other libraries (e.g. highline, slop, trollop) or roll your own.
I'm running into an issue with OptionParser's make_switch.
My code parses three arguments and runs a test to see if my MANDATORY argument is here:
#!/usr/bin/env ruby
require 'optparse'
require 'ostruct'
options = OpenStruct.new
#argv = ARGV
optparse = OptionParser.new do |opts|
#opts=opts
usage = "USAGE: ./#{File.basename($0)} [-v] -p xxxxxx"
#opts.banner = usage
#opts.on( '-p', '--pdu [PDU]', 'Specify a PDU to configure') do |res|
options.pdu = true
$pdu_name = res
end
#opts.on( '-v', '--[no-]verbose', 'Run verbosely') do
options.verbose = true
end
#opts.on( '-?', '-help','Show this message') do
puts "Help Me!"
puts #opts
exit 1
end
end
begin
if not #argv.empty?
optparse.order!(#argv)
if !options.pdu
$stderr.puts "Options -p missing."
$stderr.puts "#{#opts}\n\n"
exit 1
end
else
$stderr.puts "ERROR: Arguments Required."
$stderr.puts "#{#opts}\n\n"
exit 1
end
rescue OptionParser::InvalidOption
$stderr.puts "ERROR: Invalid option."
$stderr.puts "#{#opts}\n\n"
exit 1
end
Everything works except -?:
xxx$ ./myscript.rb -?
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/optparse.rb:451:in
`parse': missing argument: -? (OptionParser::MissingArgument)
from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/optparse.rb:1295:in `parse_in_order'
from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/optparse.rb:1254:in `catch'
from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/optparse.rb:1254:in `parse_in_order'
from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/optparse.rb:1248:in `order!'
blabla
However -help works perfectly:
xxxx$ ./myscript.rb -help
Help me!
USAGE: ./myscript.rb [-v] -p xxxxxx
-p, --pdu [PDU] Specify a PDU to configure
-v, --[no-]verbose Run verbosely
-?, -help Show this message
More surprisingly, -? -v works too:
xxxx$ ./myscript.rb -? -v
Help Me!
USAGE: ./myscript.rb [-v] -p xxxxxx
-p, --pdu [PDU] Specify a PDU to configure
-v, --[no-]verbose Run verbosely
-?, -help Show this message
What did I do wrong?
The same issue occurs if I replace -? with -h in the code.
Perhaps a quick look at the (somewhat confusing) documentation would shed some light on the situation. If you look at the docs, you'll end up at OptionParser#make_switch where you'll find an explanation of what the opt.on arguments look like:
Long style switch:
Specifies a long style switch which takes a mandatory, optional or no argument. It’s a string of the following form:
"--switch=MANDATORY" or "--switch MANDATORY"
"--switch[=OPTIONAL]"
"--switch"
Short style switch:
Specifies short style switch which takes a mandatory, optional or no argument. It’s a string of the following form:
"-xMANDATORY"
"-x[OPTIONAL]"
"-x"
Note the -xMANDATORY and then look closer at your #opts.on call:
#opts.on( '-?', '-help','Show this message') do
# ---------------^^^^^
That -help defines a -h option with a required elp argument. Presumably the option parser is interpreting that to mean that -h is an alias for -? and since -h is defined with a required argument, -? also requires an argument. If you use --help (i.e. a long style switch) then you'll probably have a better time:
#opts.on('-?', '--help', 'Show this message') do
I working from the Ruby 2.0 version but I doubt much has changed in the option parser since the older version of Ruby that you appear to be using.
In OptionParser I can make an option mandatory, but if I leave out that value it will take the name of any following option as the value, screwing up the rest of the command line parsing.
Here is a test case that echoes the values of the options:
$ ./test_case.rb --input foo --output bar
output bar
input foo
Now leave out the value for the first option:
$ ./test_case.rb --input --output bar
input --output
Is there some way to prevent it taking another option name as a value?
Thanks!
Here is the test case code:
#!/usr/bin/env ruby
require 'optparse'
files = Hash.new
option_parser = OptionParser.new do |opts|
opts.on('-i', '--input FILENAME', 'Input filename - required') do |filename|
files[:input] = filename
end
opts.on('-o', '--output FILENAME', 'Output filename - required') do |filename|
files[:output] = filename
end
end
begin
option_parser.parse!(ARGV)
rescue OptionParser::ParseError
$stderr.print "Error: " + $! + "\n"
exit
end
files.keys.each do |key|
print "#{key} #{files[key]}\n"
end
What you want to do is not a good idea. What if you really have a file named "--output"? This is a perfectly valid filename on Unix. Every Unix program's option parsing works the way the ruby one is doing, so you shouldn't change it, because then your program will be arbitrarily different from everything else, which is confusing and violates the "principle of least surprise."
The real question is: why are you having this problem in the first place? Perhaps you're running your program from another program, and the parent program is providing a blank filename as the parameter to --input, which makes it see --output as the parameter to --input. You can work around this by always quoting the filenames you pass on the command line:
./test_case.rb --input "" --output "bar"
Then --input will be blank, and that's easy to detect.
Also note that if --input is set to --output (and --output is not a real file) you can just try to open the --input file. If it fails, print a message like:
can't open input file: --output: file not found
And that should make it clear to the user what they did wrong.
try this:
opts.on('-i', '--input FILENAME', 'Input filename - required') do |filename|
files[:input] = filename
end
opts.on('-o', '--output FILENAME', 'Output filename - required') do |filename|
files[:output] = filename
end
opts.on("-h", "--help", "Show this message") do
puts opts
exit
end
begin
ARGV << "-h" if ARGV.size != 2
option_parser.parse!(ARGV)
rescue OptionParser::ParseError
$stderr.print "Error: " + $! + "\n"
exit
end
In this case, the mandatory --output option is missing, so do this after calling parse!:
unless files[:input] && files[:output]
$stderr.puts "Error: you must specify both --input and --output options."
exit 1
end
OK - this works - the regular expression in the on() call allows any string as long as it doesn't start with a '-'
If I don't pass an argument to --input and there is another option downstream then it will take that option key as the argument to --input. (e.g. --input --output). The regexp catches that and then I check the error message. If the argument it reports starts with '-' I output the correct error message, namely that there is a missing argument. Not pretty but it seems to work.
Here is my working test case:
#!/usr/bin/env ruby
require 'optparse'
files = Hash.new
option_parser = OptionParser.new do |opts|
opts.on('-i FILENAME', '--input FILENAME', /\A[^\-]+/, 'Input filename - required') do |filename|
files[:input] = filename
end
opts.on('-o FILENAME', '--output FILENAME', /\A[^\-]+/, 'Output filename - required') do |filename|
files[:output] = filename
end
end
begin
option_parser.parse!(ARGV)
rescue OptionParser::ParseError
if $!.to_s =~ /invalid\s+argument\:\s+(\-\-\S+)\s+\-/
$stderr.print "Error: missing argument: #{$1}\n"
else
$stderr.print "Error: " + $! + "\n"
end
exit
end
files.keys.each do |key|
print "#{key} #{files[key]}\n"
end