30

I'd like to be able to use ruby's OptionParser to parse sub-commands of the form

COMMAND [GLOBAL FLAGS] [SUB-COMMAND [SUB-COMMAND FLAGS]]

like:

git branch -a
gem list foo

I know I could switch to a different option parser library (like Trollop), but I'm interested in learning how to do this from within OptionParser, since I'd like to learn the library better.

Any tips?

rampion
  • 87,131
  • 49
  • 199
  • 315
  • No tips, aside from a suggestion to remain open to switching directions. In my experience, `OptionParser` has been frustrating to use for several reasons, one of them being the poor documentation -- hence your question. William Morgan, the author of Trollop, shows no mercy in his criticism (for example, see http://stackoverflow.com/questions/897630/really-cheap-command-line-option-parsing-in-ruby and http://trollop.rubyforge.org). I can't dispute what he says. – FMc Apr 29 '10 at 12:18
  • 1
    @FM: Well, like the author of that question, I'm stuck on a machine where importing libraries is a PITA, so I'm trying to make do with the standard libs - like `optparse`. – rampion Apr 29 '10 at 13:54

4 Answers4

48

Figured it out. I need to use OptionParser#order!. It will parse all the options from the start of ARGV until it finds a non-option (that isn't an option argument), removing everything it processes from ARGV, and then it will quit.

So I just need to do something like:

global = OptionParser.new do |opts|
  # ...
end
subcommands = { 
  'foo' => OptionParser.new do |opts|
     # ...
   end,
   # ...
   'baz' => OptionParser.new do |opts|
     # ...
   end
 }

 global.order!
 subcommands[ARGV.shift].order!
rampion
  • 87,131
  • 49
  • 199
  • 315
  • 7
    For reference, a more complete example is in this [Gist](https://gist.github.com/rkumar/445735). – sschuberth Apr 01 '16 at 11:04
  • What if `foo` and `baz` share a lot of common options? How to avoid repetition? – Fernando Á. Nov 07 '16 at 15:40
  • Fernando Á : simple, just abstract the common options out to a method. `def common_options(&blk) ; OptionParser.new { |opts| opts.on(...) ; ... ; blk.call(opts) } ; end`, then call that method with a block for subcommand-specific options later - `subcommands = { 'foo' => common_options { |opts| ... }, 'baz' => common_options { |opts| ... }, ... }` – rampion Nov 07 '16 at 16:08
  • Could you provide an example in code for this please? – Tom Sep 02 '22 at 14:04
1

It looks like the OptionParser syntax has changed some. I had to use the following so that the arguments array had all of the options not parsed by the opts object.

begin
  opts.order!(arguments)
rescue OptionParser::InvalidOption => io
  # Prepend the invalid option onto the arguments array
  arguments = io.recover(arguments)
rescue => e
  raise "Argument parsing failed: #{e.to_s()}"
end
jmace
  • 11
  • 1
0

GLI is the way to go, https://github.com/davetron5000/gli. An excerpt from a tutorial:

#!/usr/bin/env ruby
require 'gli'
require 'hacer'

include GLI::App

program_desc 'A simple todo list'

flag [:t,:tasklist], :default_value => File.join(ENV['HOME'],'.todolist')

pre do |global_options,command,options,args|
  $todo_list = Hacer::Todolist.new(global_options[:tasklist])
end

command :add do |c|
  c.action do |global_options,options,args|
    $todo_list.create(args)
  end
end

command :list do |c|
  c.action do
    $todo_list.list.each do |todo|
      printf("%5d - %s\n",todo.todo_id,todo.text)
    end
  end
end

command :done do |c|
  c.action do |global_options,options,args|
    id = args.shift.to_i
    $todo_list.list.each do |todo|
      $todo_list.complete(todo) if todo.todo_id == id
    end
  end
end

exit run(ARGV)

You can find the tutorial at http://davetron5000.github.io/gli/.

Alan Cabrera
  • 694
  • 1
  • 8
  • 16
0

There are also other gems you can look at such as main.

Ken Bloom
  • 57,498
  • 14
  • 111
  • 168
  • @rampion: You can look at the samples, for example http://codeforpeople.com/lib/ruby/main/main-2.8.3/samples/f.rb – Ken Bloom Apr 30 '10 at 03:51