OptionParser with subcommands - ruby

I'm trying to create a command line program with sub-commands using OptionParser. I'm following "ruby's OptionParser to get subcommands".
The problem is that it does not allow for a use case like this:
ruby main.rb --version
#=> main.rb in `<main>': undefined method `order!' for nil:NilClass (NoMethodError)
But it does allow for this:
ruby main.rb foo --options
ruby main.rb --options foo
ruby main.rb --options foo --options
How would I be properly handle command line arguments, in the case that no subcommand is given.
My example code is:
global = OptionParser.new do |opts|
opts.banner = "Usage: opt.rb [options] [subcommand [options]]"
opts.on("-v", "--version", "Print the version") do |v|
options[:version] = v
end
opts.separator ""
opts.separator subtext
end

The lines with the error:
global.order!
command = ARGV.shift
subcommands[command].order!
If global.order! uses all of ARGV, then command is nil. So... check for that.
global.order!
command = ARGV.shift
unless command
STDERR.puts "ERROR: no subcommand"
STDERR.puts global # prints usage
exit(-1)
end
subcommands[command].order!

Maybe this'll help:
require 'optparse'
VERSION = '1.0.0'
options = {}
OptionParser.new do |opt|
opt.on('-f', '--foo', 'Foo it') { |o| options[:foo] = o }
opt.on_tail('-v', '--version') do
puts VERSION
exit
end
end.parse!
puts options
Saving it as "test.rb" and running it with ruby test.rb returns:
{}
Running it with ruby test.rb -f or --foo returns:
{:foo=>true}
Running it with ruby test.rb -v or --version returns:
1.0.0
For more fun, running ruby test.rb -h or --help returns:
Usage: test [options]
-f, --foo Foo it
even though I didn't define -h or --help.
If I wanted the -v and --version flags to appear in the list then I'd change them from a on_tail method to a normal on method:
require 'optparse'
VERSION = '1.0.0'
options = {}
OptionParser.new do |opt|
opt.on('-f', '--foo', 'Foo it') { |o| options[:foo] = o }
opt.on('-v', '--version', 'Returns the version') do
puts VERSION
exit
end
end.parse!
puts options
which would return:
Usage: test [options]
-f, --foo Foo it
-v, --version Returns the version
I can add:
puts ARGV
to the end of the script and see that OptionParser is correctly handling flags and parameters:
>ruby test.rb bar --foo
{:foo=>true}
bar
>ruby test.rb --foo bar
{:foo=>true}
bar
See "Pass variables to Ruby script via command line" for more information.
There is no way your example code will handle your sample inputs using --options. No handler for --options is defined. Nor is subtext. Your code returns:
undefined local variable or method `subtext' for main:Object (NameError)
Stripping the block to:
global = OptionParser.new do |opts|
opts.on("-v", "--version", "Print the version") do |v|
options[:version] = v
end
end
and running again returns:
invalid option: --options (OptionParser::InvalidOption)
So, again, your example doesn't match the results you say you're getting.

Related

How to default to information if none is given using optparse

I have a program that creates emails, what I want to do is when the -t flag is given and no argument is given with the flag, default to something, instead it outputs the usual: <main>': missing argument: -t (OptionParser::MissingArgument)
So my question being, if I have this flag:
require 'optparse'
OPTIONS = {}
OptionParser.new do |opts|
opts.on('-t INPUT', '--type INPUT', 'Specify who to say hello to'){ |o| OPTIONS[:type] = o }
end.parse!
def say_hello
puts "Hello #{OPTIONS[:type]}"
end
case
when OPTIONS[:type]
say_hello
else
puts "Hello World"
end
and I run this flag without the required argument INPUThow do I get the program to out put the Hello World instead of the: <main>': missing argument: -t (OptionParser::MissingArgument)?
Examples:
C:\Users\bin\ruby\test_folder>ruby opt.rb -t hello
Hello hello
C:\Users\bin\ruby\test_folder>ruby opt.rb -t
opt.rb:7:in `<main>': missing argument: -t (OptionParser::MissingArgument)
C:\Users\bin\ruby\test_folder>
I figured out that by adding brackets around the INPUT I can provide the option to provide input examples:
require 'optparse'
OPTIONS = {}
OptionParser.new do |opts|
opts.on('-t [INPUT]', '--type [INPUT]', 'Specify the type of email to be generated'){ |o| OPTIONS[:type] = o }
end.parse!
def say_hello
puts "Hello #{OPTIONS[:type]}"
end
case
when OPTIONS[:type]
say_hello
else
puts "Hello World"
end
Output:
C:\Users\bin\ruby\test_folder>ruby opt.rb -t
Hello World
C:\Users\bin\ruby\test_folder>ruby opt.rb -t hello
Hello hello
So if I do this:
require 'optparse'
OPTIONS = {}
OptionParser.new do |opts|
opts.on('-t [INPUT]', '--type [INPUT]', 'Specify the type of email to be generated'){ |o| OPTIONS[:type] = o }
end.parse!
def say_hello
puts "Hello #{OPTIONS[:type]}"
puts
puts OPTIONS[:type]
end
case
when OPTIONS[:type]
say_hello
else
puts "Hello World"
puts OPTIONS[:type] unless nil; puts "No value given"
end
I can output the information provided, or when there's no information provided I can output No value given:
C:\Users\bin\ruby\test_folder>ruby opt.rb -t hello
Hello hello
hello
C:\Users\bin\ruby\test_folder>ruby opt.rb -t
Hello World
No value given

Ruby OptionParser: how to handle arguments without a prefix (like a required filename)

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"]

ruby - pipe-in from one class to another

i have a class named Foo, which uses pipe-in to read input from the command line, and it works well. i have another class named Bar, which invokes Foo, and has to feed (pipe-in) Foo in the same manner that Foo expects, but it does not seem to work for me.
see my snippet below.
i would appreciate any help.
note:
i know i can avoid doing so by passing object data from Bar to Foo, but i would like to use the pipes.
$ ls -x1
bar.rb
foo.rb
test.rb
$ cat *
# bar.rb
require "stringio"
class Bar
def self.pipe
input = StringIO.new
input.write "bar"
input.rewind
$stdin = input
Foo.print
$stdin = STDIN
end
end
# foo.rb
class Foo
##stdin = STDIN.tty? ? nil : $stdin.read #ok for cli pipe-in
def self.print
puts "stdin: #{##stdin}"
end
end
# test.rb
$:.unshift File.join(File.dirname(__FILE__))
require "foo"
require "bar"
Bar.pipe
$ echo "piped" | ruby test.rb
stdin: piped
$ ruby test.rb
stdin:
what is being done wrong, and why? a solution would be great.
i found my error. i modifying Foo as follow, but i broke its functionality.
$ cat foo.rb
# foo.rb
class Foo
def self.print
##stdin = STDIN.tty? ? nil : $stdin.read #ok for cli pipe-in
puts "stdin: #{##stdin}"
end
end
$ echo "piped" | ruby test.rb
stdin: bar

How do I call help if path undefined using OptionParser?

I'm new to Ruby and am trying to code an elegant method for setting the working directory for the program. All file accesses with be relative to this path.
The program may or may not be run from within a Git repo. I also want to provide a method of overriding the path.
I'm using OptionParser and am having difficulty getting it to set the option correctly. It seems that work_area always gets set to the Git toplevel, regardless of whether or not I'm using the --work_area flag. I've tried using a || operator within the opts.on and that didn't work either.
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: mysprogram.rb [options]"
options[:work_area] = `git rev-parse --show-toplevel --quiet 2>/dev/null`
opts.on("-d", "--work_area", String, "Override default work area.") do |wa|
options[:work_area] = wa
end
if options[:work_area]
puts "Work area is " + options[:work_area]
else
puts "ERROR: Valid work directory not found or specified."
puts opts
exit
end
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end
end.parse!
Any suggestions on what I'm doing wrong, or how to make this more Ruby-like, would be appreciated.
The block you pass to opts.on("-d" ...) is called after the block that's passed to OptionParser.new, so your puts statement is being executed before the argument parsing is actually happening. Try initializing it to the default (and testing it) outside of the OptionParser.new block entirely:
options = {}
opts = OptionParser.new do |opts|
opts.banner = "Usage: mysprogram.rb [options]"
opts.on("-d", "--work_area", String, "Override default work area.") do |wa|
options[:work_area] = wa
end
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end
end
opts.parse!
# Notice the "||=" here; this means "set options[:work_area] to a new thing
# only if it's not nil or false."
options[:work_area] ||= `git rev-parse --show-toplevel --quiet 2>/dev/null`
if options[:work_area]
puts "Work area is " + options[:work_area]
else
puts "ERROR: Valid work directory not found or specified."
puts opts
exit
end
I've always used something like:
require 'optparse'
options = {}
OptionParser.new do |opt|
opt.banner = "Usage: #{ File.basename($0) } [options]"
opt.on('--path PATH') { |o| options[:path] = o }
options[:help] = opt.help
end.parse!
puts options[:help] if !options[:path]
Saving that and running it with a --path foo option returns no output, as it should.
Running it without the --path foo option outputs:
Usage: test.rb [options]
--path PATH
Also, notice that OptionParser automatically supplies a -h or --help parameter for you if you don't define them. Calling the same code with -h results in the output you'd expect and are trying to code into your script.

How do I get only long options work in OptionParser in Ruby?

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.

Resources