如何DRYly子类(或以其他方式共享代码)将OptionParser替换为例如分享选项?

时间:2016-01-28 14:53:43

标签: ruby command-line-interface

我想分享多个脚本的某些选项,并且更喜欢使用'builtin'optparse而不是其他cli-or-optionparsing-frameworks。

我很快看了一下MRIs optparse.rb并且不明白如何最好地将OptionParser子类化(初始化程序需要一个块)。

最理想的是,我希望得到一个像这样的代码

# exe/a_script
require 'mygem'

options = {whatever: 'default'}
Mygem::OptionParser.new do |opts|
  opts.on('--whatever') do |w|
    options[:whatever] = w
  end
end.parse!

第二个脚本作为消费者:

# exe/other_script
require 'mygem'


options = {and_another: 'default'}
Mygem::OptionParser.new do |opts|
  opts.on('--and_another') do |a|
    options[:and_another] = w
  end
end.parse!

并定义“默认选项”(对于详细信息,请说“-v”,对于常见的自定义OptionParser,请说“-h”以获取帮助。

# lib/mygem/mygem_optionparser.rb
require 'optparse'
module Mygem
  class OptionParser < OptionParser
    # magic
    # define opts.on("-v") -> set options[:verbose],
    # define opts.on_tail("-h", "print help and exit") ...
  end
end

这两个脚本应该最终拥有并处理“-h”和“-v”标志,理想情况下填充“options”哈希,但可能会将其暴露给类似Mygem :: OptionParser#default_option_values。

我从哪里开始?或者是否有一种巧妙的方式来处理这种情况,例如:

# exe/b_script
OptionParser.new do |opts|
  define_custom_opts(opts)
end

我想知道我没有在这个场景中找到任何教程或示例,我认为这不是一个罕见的用例。是的,我绝对想坚持'optparse'。

更新我感到很困惑,没有看到正确的optpase-source,因此没有看到它产生自我(这让我吓了一跳:)。到目前为止很棒的答案。

2 个答案:

答案 0 :(得分:2)

我没有使用OptionParser,所以可能有更好的方法来做到这一点,但无论如何我都会采取措施。

关于OptionParser#initialize(对于我们的目的)最重要的是它给出了给定块的self。要创建一个相同的子类,我们所要做的就是使其initialize方法得到self

require 'optparse'
require 'ostruct'

module MyGem
  class OptionParser < ::OptionParser
    attr_reader :options

    def initialize(*args)
      @options = OpenStruct.new

      super *args
      default_options!

      yield(self, options) if block_given?
    end

    private
    def default_options!
      on '--whatever=WHATEVER' do |w|
        options.whatever = w
      end
    end
  end
end

这将调用super所有传递的参数,除了传递的块(如果给定)。然后它调用default_options!来创建那些默认选项(这可以通过将块传递给super来完成,但我发现上面的更清晰)。

最后,它就像超类一样产生给定的块,但它传递了第二个参数,即options对象。然后用户可以像这样使用它:

require 'my_gem/option_parser'

opts = MyGem::OptionParser.new do |parser, options|
  parser.on '--and-another=ANOTHER' do |a|
    options.another = a
  end
end

opts.parse!
p opts.options

这将为用户提供如下结果:

$ ruby script.rb --whatever=www --and-another=aaa
#<OpenStruct whatever="www", another="aaa">

作为yield(self, options)的替代方案,我们可以使用yield self,但用户需要执行以下操作:在街区内parser.options.whatever = ...

另一种方法是向&block添加initialize参数,然后执行instance_eval(&block)而不是yield。这将评估实例上下文中的块,因此用户可以直接访问options属性(以及所有其他实例方法等),例如:

parser = MyGem::OptionParser.new do
  on '--and-another=ANOTHER' do |a|
    options.another = a
  end
end

parser.parse!

然而,这有缺点,用户必须知道将在实例上下文中评估该块。我个人更喜欢明确的yield(self, options)

答案 1 :(得分:1)

您可以使用默认选项解析来定义DefaultOptParser

# default_parser.rb
require 'optparse'
require 'ostruct'

class DefaultOptParser
    attr_accessor :options

    def initialize
        @options = OpenStruct.new

        @parser = OptionParser.new do |opts|
          opts.banner = "Usage: example.rb [options]"

          opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
            options.verbose = v
          end
        end
    end

    def parse
        @parser.parse!
        @options
    end
end

p DefaultOptParser.new.parse

当你运行上面的代码时,

> ruby default_parser.rb -v
#<OpenStruct verbose=true>

接下来,定义一个上面类的子类,并添加其他选项解析。

# basic_parser.rb
require_relative "default_parser"

class BasicModeParser < DefaultOptParser
    def initialize
        super
        @parser.on("-b", "--basic-mode", "Basic mode operation") do |v|
            options.basic = v
        end
    end
end

p BasicModeParser.new.parse

当你运行上面的代码时,

> ruby basic_parser.rb -v -b
#<OpenStruct verbose=true, basic=true>

以上工作基于我目前对OptionParser的理解。