15

有没有办法在一个 Ruby 程序中多次启动OptionParser ,每个程序都有不同的选项集?

例如:

$ myscript.rb --subsys1opt a --subsys2opt b

在这里,myscript.rb 将使用 subsys1 和 subsys2,将它们的选项处理逻辑委派给它们,可能按照首先处理“a”,然后在单独的 OptionParser 对象中处理“b”的顺序;每次选择仅与该上下文相关的选项。最后一个阶段可以检查在每个部分处理完它们之后是否没有任何未知的东西。

用例是:

  1. 在松散耦合的前端程序中,各种组件具有不同的参数,我不希望'main'了解所有内容,只是将一组参数/选项委托给每个部分。

  2. 将一些更大的系统(如 RSpec)嵌入到我的应用程序中,我会简单地通过他们的选项传递一个命令行,而我的包装器不知道这些。

我也可以使用一些分隔符选项,例如----vmargs在某些 Java 应用程序中。

在 Unix 世界(startx/X、git 管道和瓷器)中有很多类似事物的真实世界示例,其中一层处理一些选项,但将其余的传播到较低层。

开箱即用,这似乎不起作用。每个OptionParse.parse!调用都会进行详尽的处理,在它不知道的任何事情上都会失败。我想我很乐意跳过未知选项。

欢迎任何提示,也许是替代方法。

4

10 回答 10

7

我需要一个永远不会抛出的解决方案OptionParser::InvalidOption,并且在当前答案中找不到优雅的解决方案。这个猴子补丁基于其他答案之一,但对其进行了清理并使其更像当前order!语义。但请参阅下面的多遍选项解析固有的未解决问题。

class OptionParser
  # Like order!, but leave any unrecognized --switches alone
  def order_recognized!(args)
    extra_opts = []
    begin
      order!(args) { |a| extra_opts << a }
    rescue OptionParser::InvalidOption => e
      extra_opts << e.args[0]
      retry
    end
    args[0, 0] = extra_opts
  end
end

就像order!except 而不是 throwing一样工作InvalidOption,它将无法识别的开关留在ARGV.

RSpec 测试:

describe OptionParser do
  before(:each) do
    @parser = OptionParser.new do |opts|
      opts.on('--foo=BAR', OptionParser::DecimalInteger) { |f| @found << f }
    end
    @found = []
  end

  describe 'order_recognized!' do
    it 'finds good switches using equals (--foo=3)' do
      argv = %w(one two --foo=3 three)
      @parser.order_recognized!(argv)
      expect(@found).to eq([3])
      expect(argv).to eq(%w(one two three))
    end

    it 'leaves unknown switches alone' do
      argv = %w(one --bar=2 two three)
      @parser.order_recognized!(argv)
      expect(@found).to eq([])
      expect(argv).to eq(%w(one --bar=2 two three))
    end

    it 'leaves unknown single-dash switches alone' do
      argv = %w(one -bar=2 two three)
      @parser.order_recognized!(argv)
      expect(@found).to eq([])
      expect(argv).to eq(%w(one -bar=2 two three))
    end

    it 'finds good switches using space (--foo 3)' do
      argv = %w(one --bar=2 two --foo 3 three)
      @parser.order_recognized!(argv)
      expect(@found).to eq([3])
      expect(argv).to eq(%w(one --bar=2 two three))
    end

    it 'finds repeated args' do
      argv = %w(one --foo=1 two --foo=3 three)
      @parser.order_recognized!(argv)
      expect(@found).to eq([1, 3])
      expect(argv).to eq(%w(one two three))
    end

    it 'maintains repeated non-switches' do
      argv = %w(one --foo=1 one --foo=3 three)
      @parser.order_recognized!(argv)
      expect(@found).to eq([1, 3])
      expect(argv).to eq(%w(one one three))
    end

    it 'maintains repeated unrecognized switches' do
      argv = %w(one --bar=1 one --bar=3 three)
      @parser.order_recognized!(argv)
      expect(@found).to eq([])
      expect(argv).to eq(%w(one --bar=1 one --bar=3 three))
    end

    it 'still raises InvalidArgument' do
      argv = %w(one --foo=bar)
      expect { @parser.order_recognized!(argv) }.to raise_error(OptionParser::InvalidArgument)
    end

    it 'still raises MissingArgument' do
      argv = %w(one --foo)
      expect { @parser.order_recognized!(argv) }.to raise_error(OptionParser::MissingArgument)
    end
  end
end

问题:通常 OptionParser 允许缩写选项,只要有足够的字符来唯一标识预期的选项。在多个阶段解析选项打破了这一点,因为 OptionParser 在第一遍中看不到所有可能的参数。例如:

describe OptionParser do
  context 'one parser with similar prefixed options' do
    before(:each) do
      @parser1 = OptionParser.new do |opts|
        opts.on('--foobar=BAR', OptionParser::DecimalInteger) { |f| @found_foobar << f }
        opts.on('--foo=BAR', OptionParser::DecimalInteger) { |f| @found_foo << f }
      end
      @found_foobar = []
      @found_foo = []
    end

    it 'distinguishes similar prefixed switches' do
      argv = %w(--foo=3 --foobar=4)
      @parser1.order_recognized!(argv)
      expect(@found_foobar).to eq([4])
      expect(@found_foo).to eq([3])
    end
  end

  context 'two parsers in separate passes' do
    before(:each) do
      @parser1 = OptionParser.new do |opts|
        opts.on('--foobar=BAR', OptionParser::DecimalInteger) { |f| @found_foobar << f }
      end
      @parser2 = OptionParser.new do |opts|
        opts.on('--foo=BAR', OptionParser::DecimalInteger) { |f| @found_foo << f }
      end
      @found_foobar = []
      @found_foo = []
    end

    it 'confuses similar prefixed switches' do
      # This is not generally desirable behavior
      argv = %w(--foo=3 --foobar=4)
      @parser1.order_recognized!(argv)
      @parser2.order_recognized!(argv)
      expect(@found_foobar).to eq([3, 4])
      expect(@found_foo).to eq([])
    end
  end
end
于 2016-11-05T03:04:24.010 回答
4

假设解析器的运行顺序已明确定义,您可以将额外的选项存储在临时全局变量中并OptionParser#parse!在每组选项上运行。

最简单的方法是使用您提到的分隔符。假设第二组参数由分隔符与第一组分隔--。然后这将做你想要的:

opts = OptionParser.new do |opts|
  # set up one OptionParser here
end

both_args = $*.join(" ").split(" -- ")
$extra_args = both_args[1].split(/\s+/)
opts.parse!(both_args[0].split(/\s+/))

然后,在第二个代码/上下文中,您可以执行以下操作:

other_opts = OptionParser.new do |opts|
  # set up the other OptionParser here
end

other_opts.parse!($extra_args)

或者,这可能是执行此操作的“更正确”的方法,您可以简单地使用OptionParser#parse,不带感叹号,这不会从$*数组中删除命令行开关,并确保没有定义选项两组相同。我建议不要$*手动修改数组,因为如果您只查看第二部分,它会使您的代码更难理解,但您可以这样做。在这种情况下,您必须忽略无效选项:

begin
    opts.parse
rescue OptionParser::InvalidOption
    puts "Warning: Invalid option"
end

正如评论中指出的那样,第二种方法实际上不起作用。$*但是,如果您无论如何都必须修改数组,则可以这样做:

tmp = Array.new

while($*.size > 0)
    begin
        opts.parse!
    rescue OptionParser::InvalidOption => e
        tmp.push(e.to_s.sub(/invalid option:\s+/,''))
    end
end

tmp.each { |a| $*.push(a) }

它不仅仅是一点点hack-y,但它应该做你想做的事。

于 2010-09-04T13:29:04.330 回答
3

我遇到了同样的问题,我找到了以下解决方案:

options = ARGV.dup
remaining = []
while !options.empty?
  begin
    head = options.shift
    remaining.concat(parser.parse([head]))
  rescue OptionParser::InvalidOption
    remaining << head
    retry
  end
end

于 2011-04-15T10:15:52.963 回答
3

对于后代,您可以使用以下order!方法执行此操作:

option_parser.order!(args) do |unrecognized_option|
  args.unshift(unrecognized_option)
end

此时,args已修改 - 所有已知选项都由 - 使用和处理option_parser- 并且可以传递给不同的选项解析器:

some_other_option_parser.order!(args) do |unrecognized_option|
  args.unshift(unrecognized_option)
end

显然,此解决方案是依赖于顺序的,但您尝试做的事情有些复杂和不寻常。

可能是一个很好的妥协的一件事是只--在命令行上使用来停止处理。这样做会留下args任何后续--,无论是更多选项还是只是常规参数。

于 2013-04-04T13:31:20.503 回答
3

我也需要同样的......我花了一段时间,但一个相对简单的方法最终效果很好。

options = {
  :input_file => 'input.txt', # default input file
}

opts = OptionParser.new do |opt|
  opt.on('-i', '--input FILE', String,
         'Input file name',
         'Default is %s' % options[:input_file] ) do |input_file|
    options[:input_file] = input_file
  end

  opt.on_tail('-h', '--help', 'Show this message') do
    puts opt
    exit
  end
end

extra_opts = Array.new
orig_args = ARGV.dup

begin
  opts.parse!(ARGV)
rescue OptionParser::InvalidOption => e
  extra_opts << e.args
  retry
end

args = orig_args & ( ARGV | extra_opts.flatten )

"args" 将包含所有命令行参数,而不是已经解析到 "options" 哈希中的参数。我将此“args”传递给要从此 ruby​​ 脚本调用的外部程序。

于 2015-08-11T10:34:25.583 回答
2

parse!即使抛出错误,另一种解决方案也依赖于对参数列表产生副作用。

让我们定义一个方法,该方法尝试使用用户定义的解析器扫描某个参数列表,并在引发 InvalidOption 错误时递归调用自身,并使用最终参数保存无效选项:

def parse_known_to(parser, initial_args=ARGV.dup)
    other_args = []                                         # this contains the unknown options
    rec_parse = Proc.new { |arg_list|                       # in_method defined proc 
        begin
            parser.parse! arg_list                          # try to parse the arg list
        rescue OptionParser::InvalidOption => e
            other_args += e.args                            # save the unknown arg
            while arg_list[0] && arg_list[0][0] != "-"      # certainly not perfect but
                other_args << arg_list.shift                # quick hack to save any parameters
            end
            rec_parse.call arg_list                         # call itself recursively
        end
    }
    rec_parse.call initial_args                             # start the rec call
    other_args                                              # return the invalid arguments
end

my_parser = OptionParser.new do
   ...
end

other_options = parse_known_to my_parser
于 2012-11-19T07:06:08.383 回答
0

我在编写一个包含 ruby​​ gem 的脚本时遇到了类似的问题,它需要自己的选项和传递给它的参数。

我想出了以下解决方案,其中它支持带有参数的包装工具选项。它通过第一个 optparser 解析它来工作,并将它不能使用的东西分成一个单独的数组(可以用另一个 optparse 再次重新解析)。

optparse = OptionParser.new do |opts|
    # OptionParser settings here
end

arguments = ARGV.dup
secondary_arguments = []

first_run = true
errors = false
while errors || first_run
  errors = false
  first_run = false
  begin
    optparse.order!(arguments) do |unrecognized_option|
      secondary_arguments.push(unrecognized_option)
    end
  rescue OptionParser::InvalidOption => e
    errors = true
    e.args.each { |arg| secondary_arguments.push(arg) }
    arguments.delete(e.args)
  end
end

primary_arguments = ARGV.dup
secondary_arguments.each do |cuke_arg|
  primary_arguments.delete(cuke_arg)
end

puts "Primary Args: #{primary_arguments}"
puts "Secondary Args: #{secondary_args}"

optparse.parse(primary_arguments)
# Can parse the second list here, if needed
# optparse_2.parse(secondary_args)

可能不是最好或最有效的方法,但它对我有用。

于 2014-05-30T17:46:35.140 回答
0

我刚从 Python 搬过来。PythonArgumentParser有很好的方法parse_known_args()。但它仍然不接受第二个参数,例如:

$ your-app -x 0 -x 1

首先-x 0是您的应用程序的论点。第二个-x 1可以属于您需要转发到的目标应用程序。ArgumentParser在这种情况下会引发错误。

现在回到 Ruby,你可以使用#order. 幸运的是,它接受无限的重复参数。例如,您需要-a-b。您的目标应用程序需要另一个强制参数(请注意-a some没有前缀-/ --)。通常#parse会忽略强制参数。但是有了#order,您将得到剩下的——太好了。请注意,您必须首先传递您自己的应用程序的参数然后是目标应用程序的参数。

$ your-app -a 0 -b 1 -a 2 some

代码应该是:

require 'optparse'
require 'ostruct'

# Build default arguments
options = OpenStruct.new
options.a = -1
options.b = -1

# Now parse arguments
target_app_argv = OptionParser.new do |opts|
    # Handle your own arguments here
    # ...
end.order

puts ' > Options         = %s' % [options]
puts ' > Target app argv = %s' % [target_app_argv]

多田:-)

于 2014-07-03T10:12:41.677 回答
0

我的尝试:

def first_parse
  left = []
  begin
    @options.order!(ARGV) do |opt|
      left << opt
    end
  rescue OptionParser::InvalidOption => e
    e.recover(args)
    left << args.shift
    retry
  end
  left
end

就我而言,我想扫描选项并选择任何可能设置调试级别、输出文件等的预定义选项。然后我将加载可能添加到选项的自定义处理器。加载完所有自定义处理器后,我调用@options.parse!(left)以处理剩余的选项。请注意,--help 内置在选项中,因此如果您不想第一次识别帮助,则需要在创建 OptParser 之前执行“OptionParser::Officious.delete('help')”,然后添加自己的帮助选项

于 2015-12-13T02:49:14.177 回答
0

解析选项直到第一个未知选项...该块可能会被多次调用,因此请确保它是安全的...

options = {
  :input_file => 'input.txt', # default input file
}

opts = OptionParser.new do |opt|
  opt.on('-i', '--input FILE', String,
    'Input file name',
    'Default is %s' % options[:input_file] ) do |input_file|
    options[:input_file] = input_file
  end

  opt.on_tail('-h', '--help', 'Show this message') do
    puts opt
    exit
  end
end

original = ARGV.dup
leftover = []

loop do
  begin
    opts.parse(original)
  rescue OptionParser::InvalidOption
    leftover.unshift(original.pop)
  else
    break
  end
end

puts "GOT #{leftover} -- #{original}"
于 2017-01-27T23:03:46.337 回答