问题 OptionParser可以跳过未知选项,稍后在Ruby程序中处理吗?


有没有办法开始 OptionParser 在一个Ruby程序中多次使用不同的选项集?

例如:

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

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

用例是:

  1. 在松散耦合的前端程序中,各种组件具有不同的参数,我不希望“main”知道所有内容,只是为每个部分委派参数/选项集。

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

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

在Unix世界中有许多类似事物的真实例子(startx / X,git plumbing和瓷器),其中一层处理一些选项但将其余部分传播到下层。

开箱即用,这似乎不起作用。每 OptionParse.parse! 呼叫将进行详尽的处理,对其不知道的任何事情失败。 我想我很乐意跳过未知选项。

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


10662
2017-09-04 12:44


起源

在上面的示例中,myscript.rb将接收所有选项作为ARGV。如果我理解你,你说其中一些选项需要传递给“子图层”。 myscript.rb会调用那些子图层吗?如果是,您的问题就变成了如何从ARGV数组中检索一些元素,将其余元素传递给另一个程序。如果myscript.rb没有调用子图层,那该怎么办? - Alkaline
是的,myscript.rb使用这些子层(更新了描述以使其更清晰)。所以你的改述问题几乎是正确的“如何从ARGV数组中检索一些元素,将其余元素传递给另一个程序”,除了它不需要另一个程序(这就是为什么我使用了更通用的子系统/组件术语),我特别要求关于'optparse'。因此“可以通过optparse跳过未知选项,稍后在ruby程序中处理吗?” - inger


答案:


假设解析器运行的顺序定义良好,您可以将额外选项存储在临时全局变量中并运行 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) }

它不仅仅是一点点黑客,但它应该做你想要的。


4
2017-09-04 13:29



是的,分隔符方法似乎是一种可行的方法,我只是希望它比调整数组好,加入和分割;也许是OptionParser直接支持的。替代解决方案的问题是,当引发该异常时,整个处理将被中止,因此后续的好参数也将被跳过。检查一下: ruby -roptparse -e 'begin OptionParser.new {|o|o.on("--ok"){puts "OK"}}.parse *ARGV;rescue OptionParser::InvalidOption;warn "BAD";end' -- --bad --ok #这说BAD虽然也应该说好。 - inger
另外,我上面的一个用例是用最少的工作来包装像RSpec这样的现有框架/系统,比如调用Spec :: Runner.run_examples,它在内部进行optparsing。所以,不幸的是,这意味着我确实重写了ARGV(即使它是不变的,并且如果可能的话,同意你避免它) - inger
似乎没有人想出一个更好的解决方案 - 所以也许确实需要一个黑客:(无论如何,现在接受这个答案:)。谢谢 - inger


对于后代,您可以使用 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
2018-04-04 13:31





我需要一个不会抛出的解决方案 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! 除了投掷 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

3
2017-11-05 03:04



干得好,@ ScottJ!这对我很有用。我自己开始走这条路,发现自己试图覆盖 order 用一个 safe_order 以及 order! 同 safe_order! 它有点失控,但你的工作得很好! - Volte
哇,我简直不敢相信 OptionParser 本机无法处理!在找到这个之前,花了将近一个小时的时间寻找如何做到这一点。好的解决方案请考虑将此提交给Ruby本身以包含在内 OptionParser! - Adam Spiers


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

options = ARGV.dup
剩余= []
while!options.empty?
  开始
    head = options.shift
    remaining.concat(parser.parse([头]))
  救援OptionParser :: InvalidOption
    剩下的<<头
    重试
  结束
结束


2
2018-04-15 10:15



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


另一个依赖的解决方案 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

2
2017-11-19 07:06





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

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
2017-08-11 10:34





当我编写一个包含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
2018-05-30 17:46





我刚刚离开了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]

田田:-)


0
2017-07-03 10:12



如果它找到一个无法识别的--foo标志,它仍会抛出OptionParser :: InvalidOption - ScottJ


我的尝试:

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
2017-12-13 02:49