OptionParser能否跳过未知选项,以便在Ruby程序中稍后处理?

18

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

例如:

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

在这里,myscript.rb将使用subsys1和subsys2,将它们的选项处理逻辑委托给它们,在可能的情况下按顺序处理'a',然后在单独的OptionParser对象中处理'b'。每次只选择与该上下文相关的选项。
最后一个阶段可以检查在每个部分处理它们之后是否还有未知的内容。
使用案例如下:
1. 在松散耦合的前端程序中,各种组件具有不同的参数,我不希望'main'了解所有内容,只需将一组参数/选项委派给每个部分即可。
2. 将一些较大的系统(例如RSpec)嵌入到我的应用程序中,并且我希望仅通过其选项传递命令行,而无需我的包装器了解这些选项。
我也可以接受某些定界符选项,例如一些Java应用程序中的--或--vmargs。
在Unix世界中有许多类似的真实世界示例(startx / X,git管道和瓷器),其中一层处理某些选项,但将其余部分传播到较低层。
开箱即用,这似乎不起作用。每个OptionParse.parse!调用都会进行详尽的处理,失败于任何它不知道的内容。我想跳过未知选项。任何提示,也许是替代方法都受欢迎。

在您上面的示例中,myscript.rb 将接收所有选项作为 ARGV。如果我理解您的意思,您说其中一些选项需要传递给“子层”。myscript.rb 是否调用这些子层? 如果是的话,您的问题就变成了如何从 ARGV 数组中检索一些元素,并将其余部分传递给另一个程序。 如果 myscript.rb 没有调用子层,那么是什么调用了呢? - Bernard
是的,myscript.rb使用那些子层(更新了描述以使其更清晰)。 因此,您重新表达的问题几乎正确,“如何从ARGV数组中检索一些元素,并将其余部分传递给另一个程序”,除了不需要另一个程序(这就是为什么我使用了更通用的子系统/组件术语),而且我特别询问了'optparse'。因此,“optparse可以跳过未知选项以便在ruby程序中稍后处理吗?” - inger
10个回答

10

我需要一个解决方案,它不会抛出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!类似,唯一的区别是如果出现无法识别的开关,则会将其保留在ARGV中,而不是抛出InvalidOption异常。

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

干得好,@ScottJ!这对我很有效。我自己也开始走这条路,发现自己试图用safe_order覆盖order,以及用safe_order!覆盖order!等等。它有点失控了,但你的方法很好! - Volte
1
哇,我简直不敢相信OptionParser不能原生处理这个!在找到这个方法之前花了将近一个小时搜索如何做。非常好的解决方案;请务必考虑将其提交给Ruby本身以包含在OptionParser中! - Adam Spiers

5

假设解析器运行的顺序是定义良好的,您可以将额外的选项存储在一个临时全局变量中,并在每组选项上运行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,但它应该能够满足您的需求。


是的,分隔符方法似乎是一种可行的方式,我只是希望它比调整数组更好,使用join和split;也许直接由OptionParser支持会更好。你替代方案的问题在于,当引发异常时,整个处理过程都会中止,因此后续的良好参数也将被跳过。请检查以下内容:ruby -roptparse -e 'begin OptionParser.new {|o|o.on("--ok"){puts "OK"}}.parse *ARGV;rescue OptionParser::InvalidOption;warn "BAD";end' -- --bad --ok # 这会显示BAD,尽管也应该显示OK。 - inger
另外,我上面的一个用例是用最小的努力包装现有的框架/系统,比如调用Spec::Runner.run_examples,它在内部进行optparsing。所以,不幸的是,这意味着我必须重写ARGV(即使它是常量,并且我同意尽可能避免它)。 - inger
似乎没有人提出更好的解决方案 - 所以也许确实需要一个hack :( 无论如何,现在先接受这个答案 :) 谢谢 - inger

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”保留跟随“--”后面的任何内容,无论是更多选项还是普通参数。

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

以上代码可以将ARGV中的选项传递给OptionParser进行处理,并将不被识别的选项存储在remaining数组中。


又一个不错的技巧,谢谢 :) 这个如何处理参数化选项?你似乎一次只看一个参数,但 OptParser 应该知道哪些需要多少个参数。 - inger
这绝对是一个好点 :) 除非用户使用 --option=value 语法,否则不适用带有参数选项。 - sylvain.joyeux

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脚本调用。

"

聪明的技巧,谢谢;很好它使用接口而不是内部工作!一些观察:1)为什么在最后要做 orig_args & ...?整个参数空间是 orig_args 的子集,所以似乎没有必要。2)如果你将 some_precious_arg --foo 传递给这个程序,some_precious_arg 将会丢失,因为解析器在遇到 --foo 之前就消耗了它,你永远得不到它。我们可以这样做:begin; abkp = ARGV.dup; ... rescue ... => e; ARGV.prepend *abkp[0...-(ARGV.size+1)]"; ... retry; end。这将恢复预解析的 ARGV,并省略有问题的 e.args[0] - csabahenk

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

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')',然后添加您自己的帮助选项。

0

我刚从Python转过来。Python的ArgumentParser有一个很棒的方法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]

完成啦 :-)


1
如果发现未识别的 --foo 标志,仍会抛出 OptionParser::InvalidOption。 - ScottJ

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)

可能不是最好或最有效的方法,但对我来说可行。


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接